Add real-time booking collaboration via WebSocket

- Backend: /ws relay endpoint (@fastify/websocket)
- Frontend: useHotelSocket hook with lock/unlock/booking events
- Calendar: lock overlay with diagonal stripes shows other manager editing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 18:05:09 +03:00
parent 1d24e5a4c4
commit 3506105fc1
7 changed files with 1919 additions and 9 deletions

View File

@@ -0,0 +1,44 @@
import { useEffect, useRef, useCallback } from 'react'
import type { Booking } from '../types'
export type WsMessage =
| { type: 'lock'; roomId: string; checkIn: string; checkOut: string; lockedBy: string }
| { type: 'unlock'; roomId: string }
| { type: 'booking:created'; booking: Booking }
| { type: 'booking:updated'; booking: Booking }
| { type: 'booking:deleted'; bookingId: string }
interface Options {
slug: string
token: string | null
onMessage: (msg: WsMessage) => void
}
export function useHotelSocket({ slug, token, onMessage }: Options) {
const wsRef = useRef<WebSocket | null>(null)
const cbRef = useRef(onMessage)
cbRef.current = onMessage
useEffect(() => {
if (!slug || !token) 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 ws = new WebSocket(`${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(token)}`)
wsRef.current = ws
ws.onmessage = (e) => {
try { cbRef.current(JSON.parse(e.data as string) as WsMessage) } catch { /* ignore */ }
}
ws.onerror = () => { /* silent */ }
return () => { ws.close(); wsRef.current = null }
}, [slug, token])
const send = useCallback((msg: WsMessage) => {
const ws = wsRef.current
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg))
}, [])
return { send }
}