diff --git a/backend/src/routes/ttlock.ts b/backend/src/routes/ttlock.ts index 52b8e33..efc8752 100644 --- a/backend/src/routes/ttlock.ts +++ b/backend/src/routes/ttlock.ts @@ -98,6 +98,35 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { return { ok: true } }) + // ── GET /api/hotels/:slug/ttlock/dll-info?workstation_id=... ──────────────── + // Читаем index.js из TTHotel чтобы узнать реальные имена функций DLL + fastify.get( + '/api/hotels/:slug/ttlock/dll-info', + { onRequest: [fastify.authenticate] }, + async (req, reply) => { + const { slug } = req.params + if (!canManage(req.user.hotelSlug, req.user.role, slug)) + return reply.code(403).send({ error: 'Forbidden' }) + const hotelId = await getHotelId(slug) + if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) + + const { workstation_id } = req.query + if (!workstation_id) return reply.code(400).send({ error: 'workstation_id required' }) + + try { + const result = await sendCommandAndWait(workstation_id, { + type: 'ttlock:read_index_js', + }, 8000) as { ok?: boolean; content?: string; path?: string; error?: string } + + if (!result.ok) return reply.code(502).send({ error: result.error ?? 'Ошибка чтения файла' }) + return { ok: true, content: result.content, path: result.path } + } catch (err) { + const msg = err instanceof Error ? err.message : 'Агент не ответил' + return reply.code(502).send({ error: msg }) + } + }, + ) + // ── POST /api/hotels/:slug/ttlock/test ────────────────────────────────────── // Проверяем: агент онлайн, энкодер отвечает на COM-порту fastify.post( diff --git a/src/lib/api.ts b/src/lib/api.ts index 8d5d3f5..777cd5d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -616,6 +616,9 @@ export const api = { testEncoder: (slug: string, workstationId: string) => req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test`, { workstationId }), + dllInfo: (slug: string, workstationId: string) => + req<{ ok: boolean; content: string; path: string }>('GET', `/api/hotels/${slug}/ttlock/dll-info?workstation_id=${workstationId}`), + getRoomLocks: (slug: string) => req('GET', `/api/hotels/${slug}/ttlock/room-locks`), diff --git a/src/pages/TTLockPage.tsx b/src/pages/TTLockPage.tsx index 1a90259..e268e2b 100644 --- a/src/pages/TTLockPage.tsx +++ b/src/pages/TTLockPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { KeyRound, Save, RefreshCw, Check, X, AlertCircle, ChevronDown, ChevronRight, Trash2, Plus, Eye, EyeOff, - Monitor, Lock, Unlock, Info, + Lock, Unlock, Info, HardDriveDownload, Zap, Terminal, } from 'lucide-react' import { api, type TTLockConfig, type RoomLockMapping, type Workstation } from '../lib/api' import { useAuth } from '../contexts/AuthContext' @@ -65,6 +65,62 @@ function Hint({ children }: { children: React.ReactNode }) { ) } +// ── Sector Picker ───────────────────────────────────────────────────────────── + +function SectorPicker({ value, onChange }: { + value: string + onChange: (v: string) => void +}) { + // Parse comma-separated string into a Set of numbers + const selected = new Set( + value.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n >= 1 && n <= 16) + ) + + const toggle = (n: number) => { + const next = new Set(selected) + if (next.has(n)) { + next.delete(n) + } else { + next.add(n) + } + // Sort numerically and join + const sorted = Array.from(next).sort((a, b) => a - b) + onChange(sorted.join(',')) + } + + return ( +
+
+ {Array.from({ length: 16 }, (_, i) => i + 1).map(n => ( + + ))} +
+

+ Выбрано: {value || '(нет)'} + {' — '}секторы Mifare-карты, которые использует TTHotel. Обычно 1–10. +

+
+ ) +} + +// ── DLL error note ──────────────────────────────────────────────────────────── +function isDllEntryError(msg: string): boolean { + return msg.includes('точку входа') || msg.toLowerCase().includes('entrypoint') || msg.toLowerCase().includes('entry point') +} + // ── Main Page ───────────────────────────────────────────────────────────────── export function TTLockPage() { @@ -91,6 +147,10 @@ export function TTLockPage() { const [wsSaving, setWsSaving] = useState>({}) const [wsTesting, setWsTesting] = useState>({}) + // Per-workstation port list state + const [wsPorts, setWsPorts] = useState>({}) + const [wsPortsLoading, setWsPortsLoading] = useState>({}) + // Форма привязки замков const [addRoomId, setAddRoomId] = useState('') const [addLockMac, setAddLockMac] = useState('') @@ -180,6 +240,18 @@ export function TTLockPage() { } } + const handleLoadPorts = async (wsId: string) => { + setWsPortsLoading(prev => ({ ...prev, [wsId]: true })) + try { + const result = await api.workstations.listPorts(slug, wsId) + setWsPorts(prev => ({ ...prev, [wsId]: result.ports })) + } catch { + // If agent offline or error, leave wsPorts[wsId] undefined — fallback to text input + } finally { + setWsPortsLoading(prev => ({ ...prev, [wsId]: false })) + } + } + if (loading) { return (
@@ -208,7 +280,17 @@ export function TTLockPage() { {error && (
- {error} +
+ {error} + {isDllEntryError(error) && ( +
+ Имена функций в DLL не совпадают.{' '} + Откройте файл{' '} + C:\Program Files (x86)\TTHotel\resources\libs\index.js{' '} + и сообщите содержимое разработчику. +
+ )} +
)} @@ -268,20 +350,10 @@ export function TTLockPage() {
- {/* Секторы карты */} + {/* Секторы карты — визуальный пикер */}
- setCardSectors(e.target.value)} - placeholder="1,2,3,4,5,6,7,8,9,10" - className="input w-full font-mono" - /> -

- Секторы Mifare-карты, которые использует TTHotel. Посмотрите в настройках TTHotel. - Обычно: 1,2,3,4,5,6,7,8,9,10 -

+
+ + + {/* Save COM port */} + + + {/* Test encoder */} + + + {/* Diagnose DLL */} + - {/* COM port input */} - setWsComPorts(prev => ({ ...prev, [ws.id]: e.target.value }))} - /> - {/* Save */} - - {/* Test */} - - - ))} + ) + })}