import { useState, useEffect } from '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' import { cn, ROOM_STATUS_COLORS, ROOM_STATUS_LABELS, HK_STATUS_COLORS, HK_STATUS_LABELS, formatCurrency } from '../lib/utils' 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() const slug = user?.hotelSlug ?? '' const [rooms, setRooms] = useState([]) const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [activeMaintenance, setActiveMaintenance] = useState>(new Set()) const [search, setSearch] = useState('') const [floorFilter, setFloorFilter] = useState('all') const [modalOpen, setModalOpen] = useState(false) const [editingRoom, setEditingRoom] = useState(undefined) const [bulkOpen, setBulkOpen] = useState(false) const [categoryFilter, setCategoryFilter] = useState('all') useEffect(() => { if (!slug) return Promise.all([ api.rooms.list(slug), api.categories.list(slug).then(rows => rows.map(c => ({ id: c.id, hotelId: c.hotel_id, name: c.name, description: c.description, color: c.color, amenities: c.amenities, photos: c.photos, } as RoomCategory))), ]) .then(([r, cats]) => { setRooms(r); setCategories(cats) }) .catch(console.error) .finally(() => setLoading(false)) api.housekeeping.list(slug, { status: 'active', category: 'maintenance' }) .then(tasks => { const ids = new Set(tasks.map(t => { const tAny = t as unknown as Record return tAny.roomId ?? tAny.room_id ?? '' }).filter(Boolean)) setActiveMaintenance(ids) }) .catch(() => {}) }, [slug]) const floors = [...new Set(rooms.map(r => r.floor))].sort() const filtered = rooms.filter(r => { const matchSearch = search === '' || r.number.includes(search) || r.type.toLowerCase().includes(search.toLowerCase()) || (r.name ?? '').toLowerCase().includes(search.toLowerCase()) const matchFloor = floorFilter === 'all' || r.floor === floorFilter const matchCat = categoryFilter === 'all' || (categoryFilter === '' ? !r.categoryId : r.categoryId === categoryFilter) return matchSearch && matchFloor && matchCat }) const stats = { available: rooms.filter(r => r.status === 'available').length, occupied: rooms.filter(r => r.status === 'occupied').length, maintenance: rooms.filter(r => r.status === 'maintenance').length, dirty: rooms.filter(r => r.housekeepingStatus === 'dirty' || r.housekeepingStatus === 'cleaning').length, } const openCreate = () => { setEditingRoom(undefined) setModalOpen(true) } const openEdit = (room: Room) => { setEditingRoom(room) setModalOpen(true) } const [saveError, setSaveError] = useState(null) const [ctxMenu, setCtxMenu] = useState<{ room: Room; x: number; y: number } | null>(null) const handleRoomQuickUpdate = async (roomId: string, patch: Partial) => { 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[] = [] for (const r of bulkRooms) { const room = await api.rooms.create(slug, r) created.push(room) } setRooms(prev => [...prev, ...created]) setBulkOpen(false) } const handleDelete = async (roomId: string) => { try { await api.rooms.delete(slug, roomId) setRooms(prev => prev.filter(r => r.id !== roomId)) setModalOpen(false) } catch (err) { const msg = err instanceof Error ? err.message : 'Ошибка удаления' setSaveError(msg) } } const handleSave = async (room: Room) => { setSaveError(null) try { const isNew = !rooms.find(r => r.id === room.id) if (isNew) { const created = await api.rooms.create(slug, { number: room.number, type: room.type, floor: room.floor, maxGuests: room.maxGuests, baseRate: room.baseRate, status: room.status, amenities: room.amenities, name: room.name, categoryId: room.categoryId, bedType: room.bedType, beds: room.beds, housekeepingStatus: room.housekeepingStatus, sortOrder: room.sortOrder, allowHourly: room.allowHourly, hourlyRate: room.hourlyRate, extraPlace: room.extraPlace, childPolicy: room.childPolicy, description: room.description, photos: room.photos, }) setRooms(prev => [...prev, created]) } else { const updated = await api.rooms.update(slug, room.id, { number: room.number, type: room.type, floor: room.floor, maxGuests: room.maxGuests, baseRate: room.baseRate, status: room.status, amenities: room.amenities, name: room.name, categoryId: room.categoryId, bedType: room.bedType, beds: room.beds, housekeepingStatus: room.housekeepingStatus, sortOrder: room.sortOrder, allowHourly: room.allowHourly, hourlyRate: room.hourlyRate, extraPlace: room.extraPlace, childPolicy: room.childPolicy, description: room.description, photos: room.photos, }) setRooms(prev => prev.map(r => r.id === updated.id ? updated : r)) } setModalOpen(false) } catch (err) { const msg = err instanceof Error ? err.message : 'Ошибка сохранения' setSaveError( msg.includes('already exists') || msg.includes('duplicate') || msg.includes('unique') ? `Номер «${room.number}» уже существует` : msg, ) } } if (loading) { return (
) } return (
{/* Header */}

Номерной фонд

{rooms.length} номеров

{/* Stats */}
{[ { label: 'Свободных', value: stats.available, color: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-900/20' }, { label: 'Занятых', value: stats.occupied, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' }, { label: 'На ремонте', value: stats.maintenance, color: 'text-orange-600 dark:text-orange-400', bg: 'bg-orange-50 dark:bg-orange-900/20' }, { label: 'Нужна уборка', value: stats.dirty, color: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-900/20' }, ].map(s => (

{s.value}

{s.label}

))}
{/* Filters — one row */}
setSearch(e.target.value)} />
{categories.length > 0 && ( )}
{/* Room grid */} {filtered.length === 0 && !loading ? (
{rooms.length === 0 ? 'Нет номеров. Добавьте первый номер.' : 'Ничего не найдено.'}
) : (
{filtered.map(room => ( openEdit(room)} onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ room, x: e.clientX, y: e.clientY }) }} /> ))}
)} {/* Room edit/create modal */} r.number)} slug={slug} onClose={() => { setModalOpen(false); setSaveError(null) }} onSave={handleSave} onDelete={editingRoom ? () => handleDelete(editingRoom.id) : undefined} categories={categories} error={saveError} /> {/* Bulk room creation modal */} r.number)} categories={categories} onClose={() => setBulkOpen(false)} onSave={handleBulkSave} /> {ctxMenu && ( setCtxMenu(null)} onStatusChange={(id, status: RoomStatus, from, to) => handleRoomQuickUpdate(id, { status, maintenanceFrom: from, maintenanceTo: to })} onHkStatusChange={(id, status: HousekeepingStatus) => handleRoomQuickUpdate(id, { housekeepingStatus: status })} onEdit={(room) => openEdit(room)} /> )}
) } function RoomCard({ room, hasActiveMaintenance, onEdit, onContextMenu }: { room: Room; hasActiveMaintenance?: boolean; onEdit: () => void; onContextMenu: (e: React.MouseEvent) => void }) { return (
{(room.status === 'maintenance' || hasActiveMaintenance) && (
{room.status === 'maintenance' && ( ремонт )} {hasActiveMaintenance && room.status !== 'maintenance' && ( неисправность )}
)}
{room.number} {room.name && ( {room.name} )}
{room.type} · Этаж {room.floor}
{ROOM_STATUS_LABELS[room.status]} {HK_STATUS_LABELS[room.housekeepingStatus]}
{room.bedType}
до {room.maxGuests}
{room.amenities.includes('Wi-Fi') && (
Wi-Fi
)}
{formatCurrency(room.baseRate)} /ночь{room.allowHourly ? ` · ${formatCurrency(room.hourlyRate ?? 0)}/ч` : ''}
{room.amenities.length > 0 && (
{room.amenities.slice(0, 3).map(a => ( {a} ))} {room.amenities.length > 3 && ( +{room.amenities.length - 3} )}
)}
) }