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

@@ -7,6 +7,7 @@ import rateLimit from '@fastify/rate-limit'
import { config } from './config'
import './types' // side-effect: augments fastify types
import wsRoutes from './routes/ws'
import authRoutes from './routes/auth'
import hotelsRoutes from './routes/hotels'
import roomsRoutes from './routes/rooms'
@@ -62,6 +63,7 @@ export async function buildApp() {
fastify.get('/health', async () => ({ status: 'ok', ts: new Date().toISOString() }))
// ── Routes ─────────────────────────────────────────────────────────────────
await fastify.register(wsRoutes)
await fastify.register(authRoutes)
await fastify.register(hotelsRoutes)
await fastify.register(roomsRoutes)

40
backend/src/routes/ws.ts Normal file
View File

@@ -0,0 +1,40 @@
import { FastifyPluginAsync } from 'fastify'
import fastifyWebsocket from '@fastify/websocket'
import type { WebSocket } from 'ws'
// hotel slug → set of connected clients
const hotelRooms = new Map<string, Set<WebSocket>>()
const ws: FastifyPluginAsync = async (fastify) => {
await fastify.register(fastifyWebsocket)
fastify.get<{ Querystring: { hotel?: string; token?: string } }>(
'/ws',
{ websocket: true },
(socket, request) => {
const hotel = request.query.hotel ?? ''
if (!hotel) { socket.close(4001, 'hotel required'); return }
if (!hotelRooms.has(hotel)) hotelRooms.set(hotel, new Set())
hotelRooms.get(hotel)!.add(socket)
socket.on('message', (raw) => {
const data = raw.toString()
const peers = hotelRooms.get(hotel)
if (!peers) return
peers.forEach(client => {
if (client !== socket && client.readyState === 1) {
client.send(data)
}
})
})
socket.on('close', () => {
hotelRooms.get(hotel)?.delete(socket)
if (hotelRooms.get(hotel)?.size === 0) hotelRooms.delete(hotel)
})
},
)
}
export default ws