import { useState, useEffect, useCallback, useRef } from 'react' import { BookingCalendar } from '../components/calendar/BookingCalendar' import { useModules } from '../contexts/ModulesContext' import { useAuth } from '../contexts/AuthContext' import { api } from '../lib/api' import type { RentalObjectApi, RentalBookingApi } from '../lib/api' import { useHotelSocket } from '../hooks/useHotelSocket' import type { WsMessage } from '../hooks/useHotelSocket' import type { Room, Booking } from '../types' import type { HkPriority } from '../components/rooms/RoomContextMenu' import type { RentalBooking } from '../data/rentalData' export type BookingLock = { checkIn: string; checkOut: string; lockedBy: string } export function CalendarPage() { const { user, session } = useAuth() const slug = user?.hotelSlug ?? '' const { statuses } = useModules() const isRentalActive = statuses['rental'] === 'active' || statuses['rental'] === 'trial' const [rooms, setRooms] = useState([]) const [bookings, setBookings] = useState([]) const [fadingBookings, setFadingBookings] = useState>(new Set()) const [rentalObjects, setRentalObjects] = useState([]) const [rentalBookings, setRentalBookings] = useState([]) const [locks, setLocks] = useState>(new Map()) const [priceOverrides, setPriceOverrides] = useState>>({}) const [hourlyPriceOverrides, setHourlyPriceOverrides] = useState>>({}) const [categoryBasePrices, setCategoryBasePrices] = useState>({}) // roomId → assigneeName for "убирается" tooltip const [cleaningAssignees, setCleaningAssignees] = useState>({}) // Set of room IDs with active maintenance tasks const [activeMaintenance, setActiveMaintenance] = useState>(new Set()) useEffect(() => { if (!slug) return const fetches: Promise[] = [ api.rooms.list(slug), api.bookings.list(slug), api.rateOverrides.list(slug).catch(() => []), api.categories.list(slug).catch(() => []), ] if (isRentalActive) { fetches.push(api.rental.listObjects(slug)) fetches.push(api.rental.listBookings(slug)) } Promise.all(fetches).then(([r, b, overridesRaw, catsRaw, ro, rb]) => { setRooms(r as Room[]) setBookings(b as Booking[]) // Build categoryId → date → price lookup const overrides = overridesRaw as import('../lib/api').RateOverrideApi[] const map: Record> = {} const hourlyMap: Record> = {} for (const o of overrides) { if (!map[o.categoryId]) map[o.categoryId] = {} map[o.categoryId][o.date] = o.price if (o.hourlyPrice != null && o.hourlyPrice > 0) { if (!hourlyMap[o.categoryId]) hourlyMap[o.categoryId] = {} hourlyMap[o.categoryId][o.date] = o.hourlyPrice } } setPriceOverrides(map) setHourlyPriceOverrides(hourlyMap) // Build category base prices const cats = catsRaw as import('../lib/api').CategoryApi[] const catBases: Record = {} for (const cat of cats) { catBases[cat.id] = { nightlyBase: cat.base_price ?? 0, hourlyBase: cat.hourly_base_price ?? 0 } } setCategoryBasePrices(catBases) if (isRentalActive) { setRentalObjects(ro as RentalObjectApi[]) setRentalBookings(rb as RentalBookingApi[]) } }).catch(console.error) // Fetch active maintenance tasks 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(() => {}) // Fetch active HK tasks to build "who is cleaning" tooltip map api.housekeeping.list(slug, { status: 'active', category: 'housekeeping' }) .then(tasks => { const assignees: Record = {} for (const t of tasks) { const tAny = t as unknown as Record if (tAny.status === 'in_progress' && tAny.roomId && tAny.assigneeName) { assignees[tAny.roomId] = tAny.assigneeName } } setCleaningAssignees(assignees) }) .catch(() => {}) }, [slug, isRentalActive]) const handleWsMessage = useCallback((msg: WsMessage) => { if (msg.type === 'lock') { setLocks(prev => new Map(prev).set(msg.roomId, { checkIn: msg.checkIn, checkOut: msg.checkOut, lockedBy: msg.lockedBy })) } else if (msg.type === 'unlock') { setLocks(prev => { const n = new Map(prev); n.delete(msg.roomId); return n }) } else if (msg.type === 'booking:created') { setBookings(prev => prev.find(b => b.id === msg.booking.id) ? prev : [...prev, msg.booking]) setLocks(prev => { const n = new Map(prev); n.delete(msg.booking.roomId); return n }) } else if (msg.type === 'booking:updated') { setBookings(prev => prev.map(b => b.id === msg.booking.id ? msg.booking : b)) } else if (msg.type === 'booking:deleted') { setBookings(prev => prev.filter(b => b.id !== msg.bookingId)) } else if (msg.type === 'housekeeping_done') { setRooms(prev => prev.map(r => r.id === msg.roomId ? { ...r, housekeepingStatus: msg.roomStatus as Room['housekeepingStatus'] } : r )) // If room is no longer being cleaned, remove from tooltip map if (msg.roomStatus !== 'cleaning') { setCleaningAssignees(prev => { const n = { ...prev }; delete n[msg.roomId]; return n }) } } else if (msg.type === 'housekeeping_task_created') { const t = msg.task as Record const roomId = t.roomId ?? t.room_id if (t.category === 'maintenance' && roomId) { setActiveMaintenance(prev => new Set([...prev, roomId])) } } else if (msg.type === 'housekeeping_updated') { const t = msg.task as Record const roomId = t.roomId ?? t.room_id // Track maintenance tasks if (t.category === 'maintenance' && roomId) { if (t.status === 'done' || t.status === 'cancelled') { setActiveMaintenance(prev => { const n = new Set(prev); n.delete(roomId); return n }) } else { setActiveMaintenance(prev => new Set([...prev, roomId])) } } if (t.roomId) { if (t.status === 'in_progress' && t.assigneeName) { setCleaningAssignees(prev => ({ ...prev, [t.roomId]: t.assigneeName })) } else if (t.status !== 'in_progress') { setCleaningAssignees(prev => { const n = { ...prev }; delete n[t.roomId]; return n }) } } } }, []) const { send, connected } = useHotelSocket({ slug, token: session?.token ?? null, onMessage: handleWsMessage }) // Clear stale locks on WS reconnect (may have missed unlock/booking:created while disconnected) const prevConnected = useRef(false) useEffect(() => { if (connected && !prevConnected.current) { setLocks(new Map()) } prevConnected.current = connected }, [connected]) const handleCreate = async (data: Partial) => { try { const created = await api.bookings.create(slug, { roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail, guestPhone: data.guestPhone, checkIn: data.checkIn, checkOut: data.checkOut, adults: data.adults, children: data.children, status: data.status, source: data.source, totalAmount: data.totalAmount, paidAmount: data.paidAmount, notes: data.notes, tariffId: data.tariffId, }) setBookings(prev => [...prev, created]) send({ type: 'booking:created', booking: created }) if (data.roomId) send({ type: 'unlock', roomId: data.roomId }) } catch (err) { console.error('Failed to create booking:', err) const msg = err instanceof Error ? err.message : 'Ошибка создания бронирования' alert(msg) } } const handleUpdate = async (id: string, data: Partial) => { try { const updated = await api.bookings.update(slug, id, { roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail, checkIn: data.checkIn, checkOut: data.checkOut, adults: data.adults, children: data.children, status: data.status, source: data.source, totalAmount: data.totalAmount, notes: data.notes, tariffId: data.tariffId, }) setBookings(prev => prev.map(b => b.id === id ? updated : b)) send({ type: 'booking:updated', booking: updated }) if (data.status === 'cancelled') { setFadingBookings(prev => new Set([...prev, id])) setTimeout(() => { setBookings(prev => prev.filter(b => b.id !== id)) setFadingBookings(prev => { const n = new Set(prev); n.delete(id); return n }) }, 900) } } catch (err) { console.error('Failed to update booking:', err) } } const handleBulkUpdate = async (updates: Array<{ id: string; data: Partial }>) => { try { const results = await Promise.all( updates.map(u => api.bookings.update(slug, u.id, { status: u.data.status, checkIn: u.data.checkIn, checkOut: u.data.checkOut, roomId: u.data.roomId, })), ) setBookings(prev => { let next = [...prev] results.forEach(r => { next = next.map(b => b.id === r.id ? r : b) }) return next }) results.forEach(r => send({ type: 'booking:updated', booking: r })) } catch (err) { console.error('Failed to bulk update bookings:', err) } } const handleDraftStart = useCallback((roomId: string, checkIn: string, checkOut: string) => { send({ type: 'lock', roomId, checkIn, checkOut, lockedBy: user?.name ?? 'Менеджер' }) }, [send, user?.name]) const handleDraftCancel = useCallback((roomId: string) => { send({ type: 'unlock', roomId }) }, [send]) const handleRoomUpdate = useCallback(async (roomId: string, patch: Partial, priority?: HkPriority) => { try { const updated = await api.rooms.update(slug, roomId, patch) setRooms(prev => prev.map(r => r.id === updated.id ? updated : r)) if (patch.housekeepingStatus === 'dirty') { const today = new Date().toISOString().slice(0, 10) const task = await api.housekeeping.create(slug, { room_id: roomId, type: 'cleaning', priority: priority ?? 'medium', due_date: today, category: 'housekeeping', }) send({ type: 'housekeeping_task_created', task: task as unknown as Record }) } } catch (err) { console.error('Failed to update room:', err) } }, [slug, send]) const handleMaintenanceTaskCreate = useCallback(async (roomId: string, description: string, priority: HkPriority, photos: string[] = []) => { try { const today = new Date().toISOString().slice(0, 10) const task = await api.housekeeping.create(slug, { room_id: roomId, type: 'maintenance', priority, notes: description, due_date: today, category: 'maintenance', photos, }) send({ type: 'housekeeping_task_created', task: task as unknown as Record }) } catch (err) { console.error('Failed to create maintenance task:', err) } }, [slug, send]) const handleRentalBookingCreate = useCallback(async (b: RentalBooking) => { try { const created = await api.rental.createBooking(slug, { object_id: b.objectId, date: b.date, is_full_day: b.isFullDay, start_hour: b.startHour, end_hour: b.endHour, guest_name: b.guestName, guest_phone: b.guestPhone ?? '', linked_room_id: b.linkedRoomId, total_amount: b.totalAmount, paid_amount: b.paidAmount ?? 0, status: b.status, notes: b.notes, }) setRentalBookings(prev => [...prev, created]) } catch (err) { console.error('Failed to create rental booking:', err) const msg = err instanceof Error ? err.message : 'Ошибка сохранения аренды' alert(msg) } }, [slug]) return (
) }