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

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { BedDouble, Users, Wifi, Plus, Search, Loader2, Pencil, Layers } from 'lucide-react'
import { BedDouble, Users, Wifi, Plus, Search, Loader2, Pencil, Layers, Wrench } from 'lucide-react'
import { useAuth } from '../contexts/AuthContext'
import { api } from '../lib/api'
import type { Room, RoomCategory } from '../types'
@@ -7,6 +7,8 @@ import { cn, ROOM_STATUS_COLORS, ROOM_STATUS_LABELS, HK_STATUS_COLORS, HK_STATUS
import { Badge } from '../components/ui/Badge'
import { RoomModal } from '../components/rooms/RoomModal'
import { BulkRoomModal, type BulkRoomData } from '../components/rooms/BulkRoomModal'
import { RoomContextMenu } from '../components/rooms/RoomContextMenu'
import type { RoomStatus, HousekeepingStatus } from '../types'
export function RoomsPage() {
const { user } = useAuth()
@@ -69,6 +71,16 @@ export function RoomsPage() {
}
const [saveError, setSaveError] = useState<string | null>(null)
const [ctxMenu, setCtxMenu] = useState<{ room: Room; x: number; y: number } | null>(null)
const handleRoomQuickUpdate = async (roomId: string, patch: Partial<Room>) => {
try {
const updated = await api.rooms.update(slug, roomId, patch)
setRooms(prev => prev.map(r => r.id === updated.id ? updated : r))
} catch (err) {
console.error('Failed to update room:', err)
}
}
const handleBulkSave = async (bulkRooms: BulkRoomData[]) => {
const created: Room[] = []
@@ -213,7 +225,12 @@ export function RoomsPage() {
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filtered.map(room => (
<RoomCard key={room.id} room={room} onEdit={() => openEdit(room)} />
<RoomCard
key={room.id}
room={room}
onEdit={() => openEdit(room)}
onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ room, x: e.clientX, y: e.clientY }) }}
/>
))}
</div>
)}
@@ -239,19 +256,41 @@ export function RoomsPage() {
onClose={() => setBulkOpen(false)}
onSave={handleBulkSave}
/>
{ctxMenu && (
<RoomContextMenu
room={ctxMenu.room}
x={ctxMenu.x}
y={ctxMenu.y}
onClose={() => setCtxMenu(null)}
onStatusChange={(id, status: RoomStatus) => handleRoomQuickUpdate(id, { status })}
onHkStatusChange={(id, status: HousekeepingStatus) => handleRoomQuickUpdate(id, { housekeepingStatus: status })}
onEdit={(room) => openEdit(room)}
/>
)}
</div>
)
}
function RoomCard({ room, onEdit }: { room: Room; onEdit: () => void }) {
function RoomCard({ room, onEdit, onContextMenu }: { room: Room; onEdit: () => void; onContextMenu: (e: React.MouseEvent) => void }) {
return (
<div className="card p-4 hover:shadow-card-hover transition-shadow cursor-pointer group relative">
<div
className="card p-4 hover:shadow-card-hover transition-shadow cursor-pointer group relative"
onContextMenu={onContextMenu}
>
<button
onClick={e => { e.stopPropagation(); onEdit() }}
className="absolute top-3 right-3 p-1.5 rounded-lg bg-white dark:bg-slate-700 border border-slate-200 dark:border-slate-600 text-slate-500 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm hover:text-brand-600"
>
<Pencil size={13} />
</button>
{room.status === 'maintenance' && (
<div className="absolute top-3 left-3">
<span className="flex items-center gap-1 text-[10px] font-medium text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20 px-1.5 py-0.5 rounded-md border border-orange-200 dark:border-orange-800">
<Wrench size={10} />ремонт
</span>
</div>
)}
<div className="flex items-start justify-between mb-3">
<div>