diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index 3e867ce..35f2081 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -152,12 +152,23 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd }, [dragStart]) const handleMouseMove = useCallback((e: React.MouseEvent) => { - if (!pendingMoveBooking.current || movingBookingRef.current) return - const dx = Math.abs(e.clientX - (moveStartPos.current?.x ?? 0)) - const dy = Math.abs(e.clientY - (moveStartPos.current?.y ?? 0)) - if (dx > 5 || dy > 5) { - setMovingBooking(pendingMoveBooking.current) - setMoveTargetRoomId(pendingMoveBooking.current.roomId) + // Phase 1: detect drag start + if (pendingMoveBooking.current && !movingBookingRef.current) { + const dx = Math.abs(e.clientX - (moveStartPos.current?.x ?? 0)) + const dy = Math.abs(e.clientY - (moveStartPos.current?.y ?? 0)) + if (dx > 5 || dy > 5) { + setMovingBooking(pendingMoveBooking.current) + setMoveTargetRoomId(pendingMoveBooking.current.roomId) + } + return + } + // Phase 2: track which room row the cursor is over + if (movingBookingRef.current) { + const el = document.elementFromPoint(e.clientX, e.clientY) + const rowEl = el?.closest('[data-room-id]') as HTMLElement | null + if (rowEl?.dataset.roomId) { + setMoveTargetRoomId(rowEl.dataset.roomId) + } } }, []) @@ -429,12 +440,12 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd rows.push(
{ if (movingBookingRef.current) setMoveTargetRoomId(room.id) }} > {/* Room label */}
void } +function getFreshToken(): string | null { + try { + const s = localStorage.getItem('hotelsync-session') + return s ? (JSON.parse(s) as { token: string }).token : null + } catch { + return null + } +} + export function useHotelSocket({ slug, token, onMessage }: Options) { const wsRef = useRef(null) const cbRef = useRef(onMessage) @@ -30,14 +39,32 @@ export function useHotelSocket({ slug, token, onMessage }: Options) { let destroyed = false let reconnectTimer: ReturnType | null = null + let pingTimer: ReturnType | null = null let attempts = 0 + function startPing(ws: WebSocket) { + if (pingTimer) clearInterval(pingTimer) + // Send a ping every 30s to keep nginx / proxies from closing idle connection + pingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'ping' })) + } + }, 30_000) + } + function connect() { if (destroyed) return + // Always read fresh token so reconnects after 1h token expiry use the refreshed JWT + const currentToken = getFreshToken() ?? token + if (!currentToken) { + console.warn('[WS] no token available') + return + } + const base = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://api.hotelsync.ru' const wsBase = base.replace(/^https/, 'wss').replace(/^http(?!s)/, 'ws') - const url = `${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(token!)}` + const url = `${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(currentToken)}` console.log('[WS] connecting to', url.replace(/token=[^&]+/, 'token=***')) const ws = new WebSocket(url) @@ -47,11 +74,17 @@ export function useHotelSocket({ slug, token, onMessage }: Options) { if (destroyed) { ws.close(); return } setConnected(true) attempts = 0 + startPing(ws) console.log('[WS] connected ✓ hotel:', slug) } ws.onmessage = (e) => { - try { cbRef.current(JSON.parse(e.data as string) as WsMessage) } catch { /* ignore */ } + try { + const msg = JSON.parse(e.data as string) as WsMessage + // ignore pong / ping from server + if ((msg as unknown as { type: string }).type === 'pong') return + cbRef.current(msg) + } catch { /* ignore */ } } ws.onerror = (ev) => { @@ -61,6 +94,7 @@ export function useHotelSocket({ slug, token, onMessage }: Options) { ws.onclose = (ev) => { wsRef.current = null setConnected(false) + if (pingTimer) { clearInterval(pingTimer); pingTimer = null } if (destroyed) return const delay = Math.min(1000 * 2 ** attempts, 30_000) attempts++ @@ -74,6 +108,7 @@ export function useHotelSocket({ slug, token, onMessage }: Options) { return () => { destroyed = true if (reconnectTimer) clearTimeout(reconnectTimer) + if (pingTimer) clearInterval(pingTimer) wsRef.current?.close() wsRef.current = null setConnected(false)