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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user