feat: right-click context menu for room status + bed type capacity

- Add RoomContextMenu component (portal, Escape/click-outside to close)
  - Change room status: available / maintenance / blocked
  - Change housekeeping status: clean / dirty / cleaning / inspect
  - Open room edit (optional)
- Calendar: right-click on room label → context menu; onRoomUpdate prop
  wired to CalendarPage (calls api.rooms.update); show 🔧/🔒 badge on label
- Rooms page: right-click on any room card → same context menu;
  show orange "ремонт" badge on maintenance rooms
- BedTypesContext: add capacity field (max guests per bed type)
- BedTypes reference UI: add capacity input to add/edit form; show "N чел."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 14:30:14 +03:00
parent 8c6cdfb1c7
commit fd50e02be0
6 changed files with 252 additions and 33 deletions

View File

@@ -4,11 +4,12 @@ import { addDays, format, startOfDay, differenceInDays, parseISO, isToday } from
import { ru } from 'date-fns/locale'
import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown, AlignJustify, Clock } from 'lucide-react'
import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils'
import type { Room, Booking, DraftBooking } from '../../types'
import type { Room, Booking, DraftBooking, RoomStatus, HousekeepingStatus } from '../../types'
import type { RentalObject, RentalBooking } from '../../data/rentalData'
import { BookingModal } from '../bookings/BookingModal'
import { BookingDetailPanel } from '../bookings/BookingDetailPanel'
import { RentalBookingModal } from '../rental/RentalBookingModal'
import { RoomContextMenu } from '../rooms/RoomContextMenu'
const CELL_WIDTH = 52
const ROW_HEIGHT = 56
@@ -25,6 +26,7 @@ interface BookingCalendarProps {
onBookingCreate: (b: Partial<Booking>) => void
onBookingUpdate: (id: string, b: Partial<Booking>) => void
onBookingBulkUpdate?: (updates: Array<{ id: string; data: Partial<Booking> }>) => void
onRoomUpdate?: (roomId: string, patch: Partial<Room>) => void
fadingBookingIds?: Set<string>
rentalObjects?: RentalObject[]
rentalBookings?: RentalBooking[]
@@ -56,10 +58,26 @@ function getRoomTypeColor(type: string): string {
return map[type] ?? 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300'
}
export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBookingUpdate, onBookingBulkUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate, locks = new Map(), onDraftStart, onDraftCancel, wsConnected }: BookingCalendarProps) {
export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBookingUpdate, onBookingBulkUpdate, onRoomUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate, locks = new Map(), onDraftStart, onDraftCancel, wsConnected }: BookingCalendarProps) {
const [startDate, setStartDate] = useState(() => startOfDay(new Date()))
const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE)
// Room context menu
const [ctxMenu, setCtxMenu] = useState<{ room: Room; x: number; y: number } | null>(null)
const handleRoomContextMenu = (e: React.MouseEvent, room: Room) => {
e.preventDefault()
setCtxMenu({ room, x: e.clientX, y: e.clientY })
}
const handleCtxStatusChange = (roomId: string, status: RoomStatus) => {
onRoomUpdate?.(roomId, { status })
}
const handleCtxHkChange = (roomId: string, status: HousekeepingStatus) => {
onRoomUpdate?.(roomId, { housekeepingStatus: status })
}
// Date picker state
const [showNavPicker, setShowNavPicker] = useState(false)
const [pickerDateInput, setPickerDateInput] = useState(format(new Date(), 'yyyy-MM-dd'))
@@ -429,14 +447,21 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
>
{/* Room label */}
<div
className={cn('shrink-0 sticky left-0 z-10 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex items-center gap-2 group-hover:bg-slate-50 dark:group-hover:bg-slate-800', isMobile ? 'px-2' : 'px-3')}
className={cn('shrink-0 sticky left-0 z-10 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex items-center gap-2 group-hover:bg-slate-50 dark:group-hover:bg-slate-800 cursor-context-menu select-none', isMobile ? 'px-2' : 'px-3')}
style={{ width: labelWidth }}
onContextMenu={e => handleRoomContextMenu(e, room)}
>
<div>
<div className="flex items-center gap-1.5">
<span className="text-sm font-bold text-slate-900 dark:text-slate-100">{room.number}</span>
{room.name && !isMobile && <span className="text-xs text-slate-500 dark:text-slate-400">{room.name}</span>}
</div>
{room.status === 'maintenance' && (
<span className="text-[10px] text-orange-500 font-medium leading-none">🔧 ремонт</span>
)}
{room.status === 'blocked' && (
<span className="text-[10px] text-slate-400 font-medium leading-none">🔒 закрыт</span>
)}
</div>
</div>
@@ -804,6 +829,17 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
}}
/>
)}
{ctxMenu && (
<RoomContextMenu
room={ctxMenu.room}
x={ctxMenu.x}
y={ctxMenu.y}
onClose={() => setCtxMenu(null)}
onStatusChange={handleCtxStatusChange}
onHkStatusChange={handleCtxHkChange}
/>
)}
</div>
)
}