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:
118
src/components/rooms/RoomContextMenu.tsx
Normal file
118
src/components/rooms/RoomContextMenu.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Wrench, Ban, CheckCircle, Sparkles, ClipboardList, Pencil } from 'lucide-react'
|
||||
import type { Room, RoomStatus, HousekeepingStatus } from '../../types'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
interface RoomContextMenuProps {
|
||||
room: Room
|
||||
x: number
|
||||
y: number
|
||||
onClose: () => void
|
||||
onStatusChange: (roomId: string, status: RoomStatus) => void
|
||||
onHkStatusChange: (roomId: string, status: HousekeepingStatus) => void
|
||||
onEdit?: (room: Room) => void
|
||||
}
|
||||
|
||||
const STATUS_ITEMS: { status: RoomStatus; label: string; icon: React.ReactNode; color: string }[] = [
|
||||
{ status: 'available', label: 'Свободен', icon: <CheckCircle size={14} />, color: 'text-emerald-600 dark:text-emerald-400' },
|
||||
{ status: 'maintenance', label: 'На ремонт', icon: <Wrench size={14} />, color: 'text-orange-600 dark:text-orange-400' },
|
||||
{ status: 'blocked', label: 'Закрыт', icon: <Ban size={14} />, color: 'text-slate-500 dark:text-slate-400' },
|
||||
]
|
||||
|
||||
const HK_ITEMS: { status: HousekeepingStatus; label: string; icon: React.ReactNode; color: string }[] = [
|
||||
{ status: 'clean', label: 'Чисто', icon: <Sparkles size={14} />, color: 'text-emerald-600 dark:text-emerald-400' },
|
||||
{ status: 'dirty', label: 'Убрать', icon: <ClipboardList size={14} />, color: 'text-red-500 dark:text-red-400' },
|
||||
{ status: 'cleaning', label: 'Убирается', icon: <ClipboardList size={14} />, color: 'text-blue-500 dark:text-blue-400' },
|
||||
{ status: 'inspect', label: 'Проверить', icon: <ClipboardList size={14} />, color: 'text-amber-600 dark:text-amber-400' },
|
||||
]
|
||||
|
||||
export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatusChange, onEdit }: RoomContextMenuProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Adjust position so menu doesn't go off screen
|
||||
const menuX = Math.min(x, window.innerWidth - 220)
|
||||
const menuY = Math.min(y, window.innerHeight - 280)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose()
|
||||
}
|
||||
const keyHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
||||
document.addEventListener('mousedown', handler)
|
||||
document.addEventListener('keydown', keyHandler)
|
||||
return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('keydown', keyHandler) }
|
||||
}, [onClose])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={ref}
|
||||
className="fixed z-[9999] w-52 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-xl shadow-xl overflow-hidden select-none"
|
||||
style={{ left: menuX, top: menuY }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-slate-100 dark:border-slate-700">
|
||||
<p className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Номер {room.number}</p>
|
||||
</div>
|
||||
|
||||
{/* Room status */}
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="px-2 py-1 text-[10px] font-semibold text-slate-400 uppercase tracking-wide">Статус номера</p>
|
||||
{STATUS_ITEMS.map(item => (
|
||||
<button
|
||||
key={item.status}
|
||||
onClick={() => { onStatusChange(room.id, item.status); onClose() }}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700',
|
||||
item.color,
|
||||
room.status === item.status && 'bg-slate-100 dark:bg-slate-700 font-medium',
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
{room.status === item.status && <span className="ml-auto text-[10px] text-slate-400">✓</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-100 dark:border-slate-700 mx-2" />
|
||||
|
||||
{/* Housekeeping status */}
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="px-2 py-1 text-[10px] font-semibold text-slate-400 uppercase tracking-wide">Уборка</p>
|
||||
{HK_ITEMS.map(item => (
|
||||
<button
|
||||
key={item.status}
|
||||
onClick={() => { onHkStatusChange(room.id, item.status); onClose() }}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700',
|
||||
item.color,
|
||||
room.housekeepingStatus === item.status && 'bg-slate-100 dark:bg-slate-700 font-medium',
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
{room.housekeepingStatus === item.status && <span className="ml-auto text-[10px] text-slate-400">✓</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{onEdit && (
|
||||
<>
|
||||
<div className="border-t border-slate-100 dark:border-slate-700 mx-2" />
|
||||
<div className="px-2 py-1.5">
|
||||
<button
|
||||
onClick={() => { onEdit(room); onClose() }}
|
||||
className="w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
Редактировать номер
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user