feat: TTLock integration + fix agent offline status

- Add TTLock electronic lock module: settings page, room-lock mapping,
  card issuance via agent command (ttlock:write_card)
- Fix agent offline: EquipmentPage now polls every 30s + reacts to
  workstation:online/offline WebSocket events instantly
- Backend broadcasts workstation online/offline events to hotel clients
  via existing WebSocket (agent-ws.ts)
- New DB migration 048_ttlock.sql: hotel_ttlock_config, room_lock_mappings,
  card_issuances tables
- New backend routes: GET/PATCH ttlock/config, POST ttlock/token,
  GET ttlock/locks, POST/DELETE ttlock/room-locks, POST issue-card
- "Выдать ключ (TTLock)" button in BookingDetailPanel for confirmed/checked_in
- TTLock page added to sidebar under Настройки

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 15:46:17 +03:00
parent 7a3772d1cf
commit be92d026e8
11 changed files with 1095 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ import {
import { cn } from '../lib/utils'
import { api, type Workstation, type WorkstationDevice } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import { useHotelSocket } from '../hooks/useHotelSocket'
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -1024,7 +1025,7 @@ function WorkstationCard({
// ─── Main Page ────────────────────────────────────────────────────────────────
export function EquipmentPage() {
const { user } = useAuth()
const { user, session } = useAuth()
const slug = user?.hotelSlug ?? ''
const [workstations, setWorkstations] = useState<Workstation[]>([])
@@ -1051,6 +1052,23 @@ export function EquipmentPage() {
useEffect(() => { load() }, [load])
// Auto-refresh every 30 s so online/offline status stays current
useEffect(() => {
const timer = setInterval(load, 30_000)
return () => clearInterval(timer)
}, [load])
// Instant update when agent connects or disconnects
useHotelSocket({
slug,
token: session?.token ?? null,
onMessage: (msg) => {
if (msg.type === 'workstation:online' || msg.type === 'workstation:offline') {
load()
}
},
})
useEffect(() => {
api.agents.getLatestRelease().then(setLatestRelease).catch(() => {})
}, [])