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)