feat: 3 improvements — hourly toggle position, category rooms panel, period prices in grid

1. BookingModal: move Посуточно/Почасово toggle to top (before dates)
   when room has hourly rate enabled
2. RoomCategoriesPage: Номера > button now expands inline panel
   showing rooms belonging to that category
3. AvailabilityPage:
   - Period prices auto-applied to grid (base → periods → overrides),
     cells from periods show violet П badge
   - П legend item added
   - Hourly rate settings shown in Periods tab for hourly-enabled rooms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 20:32:32 +03:00
parent 2b21f2c219
commit b3d143f1dd
4 changed files with 184 additions and 47 deletions

View File

@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'
import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight, ChevronDown, ChevronUp, Check, Loader2 } from 'lucide-react'
import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight, ChevronDown, ChevronUp, Check, Loader2, DoorOpen } from 'lucide-react'
import { cn } from '../lib/utils'
import type { RoomCategory } from '../types'
import type { RoomCategory, Room } from '../types'
import { useAmenities } from '../contexts/AmenitiesContext'
import { useBedTypes } from '../contexts/BedTypesContext'
import { useAuth } from '../contexts/AuthContext'
@@ -353,6 +353,8 @@ export function RoomCategoriesPage() {
const [categories, setCategories] = useState<RoomCategory[]>([])
const [loading, setLoading] = useState(true)
const [rooms, setRooms] = useState<Room[]>([])
const [catRoomsId, setCatRoomsId] = useState<string | null>(null)
const [editingCat, setEditingCat] = useState<RoomCategory | undefined>(undefined)
const [formOpen, setFormOpen] = useState(false)
@@ -376,9 +378,13 @@ export function RoomCategoriesPage() {
useEffect(() => {
if (!slug) return
api.categories.list(slug)
.then(rows => setCategories(rows.map(fromApi)))
.catch(console.error)
Promise.all([
api.categories.list(slug),
api.rooms.list(slug).catch(() => []),
]).then(([rows, roomRows]) => {
setCategories(rows.map(fromApi))
setRooms(roomRows)
}).catch(console.error)
.finally(() => setLoading(false))
}, [slug])
@@ -499,14 +505,44 @@ export function RoomCategoriesPage() {
</button>
</div>
<button
onClick={() => openEdit(cat)}
className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-medium hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors whitespace-nowrap"
onClick={() => setCatRoomsId(catRoomsId === cat.id ? null : cat.id)}
className={cn(
'flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors whitespace-nowrap',
catRoomsId === cat.id
? 'bg-brand-100 dark:bg-brand-900/30 text-brand-700 dark:text-brand-300'
: 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600',
)}
>
<span className="hidden sm:inline">Номера</span>
<ChevronRight size={12} />
<ChevronRight size={12} className={cn('transition-transform', catRoomsId === cat.id && 'rotate-90')} />
</button>
</div>
</div>
{/* Rooms list panel */}
{catRoomsId === cat.id && (() => {
const catRooms = rooms.filter(r => r.categoryId === cat.id)
return (
<div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
{catRooms.length === 0 ? (
<p className="text-sm text-slate-400 flex items-center gap-2">
<DoorOpen size={15} />
Нет номеров в этой категории
</p>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
{catRooms.sort((a, b) => a.sortOrder - b.sortOrder).map(r => (
<div key={r.id} className="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-50 dark:bg-slate-700/50 text-sm">
<DoorOpen size={13} className="text-slate-400 shrink-0" />
<span className="font-medium text-slate-700 dark:text-slate-200">{r.number}</span>
<span className="text-slate-400 text-xs truncate">{r.baseRate.toLocaleString('ru-RU')} </span>
</div>
))}
</div>
)}
</div>
)
})()}
</div>
)
})}