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

@@ -601,6 +601,36 @@ export const api = {
regenerateToken: (slug: string) =>
req<{ apiToken: string }>('POST', `/api/hotels/${slug}/wifi-settings/regenerate-token`),
},
// ── TTLock ─────────────────────────────────────────────────────────────────
ttlock: {
getConfig: (slug: string) =>
req<TTLockConfig>('GET', `/api/hotels/${slug}/ttlock/config`),
updateConfig: (slug: string, data: TTLockConfigUpdate) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/ttlock/config`, data),
testToken: (slug: string) =>
req<{ ok: boolean; expiresAt: string }>('POST', `/api/hotels/${slug}/ttlock/token`),
getLocks: (slug: string) =>
req<TTLockDevice[]>('GET', `/api/hotels/${slug}/ttlock/locks`),
getRoomLocks: (slug: string) =>
req<RoomLockMapping[]>('GET', `/api/hotels/${slug}/ttlock/room-locks`),
mapRoom: (slug: string, data: { roomId: string; lockId: number; lockName?: string }) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/room-locks`, data),
unmapRoom: (slug: string, roomId: string) =>
req<{ ok: boolean }>('DELETE', `/api/hotels/${slug}/ttlock/room-locks/${roomId}`),
issueCard: (slug: string, bookingId: string) =>
req<CardIssuanceResult>('POST', `/api/hotels/${slug}/bookings/${bookingId}/issue-card`),
getCards: (slug: string, bookingId: string) =>
req<CardIssuance[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/cards`),
},
}
// ── Schedule ─────────────────────────────────────────────────────────────────
@@ -1051,6 +1081,58 @@ export interface Workstation {
devices: WorkstationDevice[] | null
}
export interface TTLockConfig {
isEnabled: boolean
clientId: string
ttlockUsername: string
workstationId: string | null
hasToken: boolean
tokenExpiresAt: string | null
}
export interface TTLockConfigUpdate {
isEnabled?: boolean
clientId?: string
clientSecret?: string
ttlockUsername?: string
ttlockPassword?: string
workstationId?: string | null
}
export interface TTLockDevice {
lockId: number
name: string
}
export interface RoomLockMapping {
roomId: string
roomName: string
roomNumber: string
lockId: number
lockName: string | null
}
export interface CardIssuanceResult {
ok: boolean
issuanceId: string
issuedAt: string
lockName: string | null
roomName: string
checkIn: string
checkOut: string
}
export interface CardIssuance {
id: string
lockId: number
issuedAt: string
issuedBy: string | null
checkIn: string
checkOut: string
isRevoked: boolean
revokedAt: string | null
}
export interface WifiSettings {
id: string
hotelId: string