From 1358dda7f94657550d6274876a23d2b2f64e3585 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Fri, 27 Mar 2026 18:30:06 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Equipment=20page=20=E2=80=94=20KKT=20hi?= =?UTF-8?q?des=20connection,=20Windows=20printer,=20COM=20retry;=20agent?= =?UTF-8?q?=20update=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EquipmentPage: KKT hides USB/COM/Network buttons (DTO handles connection) - EquipmentPage: Printer adds Windows connection type — lists installed printers from agent - EquipmentPage: COM port section has retry button (↺) - api.ts: add listPrinters, extend connection type - workstations.ts: add /printers endpoint (list_printers command) Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/workstations.ts | 12 +++ src/lib/api.ts | 4 +- src/pages/EquipmentPage.tsx | 117 +++++++++++++++++++++++++---- 3 files changed, 116 insertions(+), 17 deletions(-) diff --git a/backend/src/routes/workstations.ts b/backend/src/routes/workstations.ts index b4f8df5..d177871 100644 --- a/backend/src/routes/workstations.ts +++ b/backend/src/routes/workstations.ts @@ -262,6 +262,18 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => { }, ) + // ── GET /api/hotels/:slug/workstations/:id/printers ───────────────────────── + fastify.get('/api/hotels/:slug/workstations/:id/printers', { onRequest: [fastify.authenticate] }, async (req, reply) => { + const { id } = req.params + try { + const result = await sendCommandAndWait(id, { type: 'list_printers' }, 8000) as { ok: boolean; printers?: { name: string; isDefault: boolean }[]; error?: string } + if (!result.ok) return reply.code(502).send({ error: result.error ?? 'Агент недоступен' }) + return { printers: result.printers ?? [] } + } catch (err) { + return reply.code(502).send({ error: err instanceof Error ? err.message : 'Ошибка' }) + } + }) + // ── GET /api/hotels/:slug/workstations/:id/ports ──────────────────────────── fastify.get( '/api/hotels/:slug/workstations/:id/ports', diff --git a/src/lib/api.ts b/src/lib/api.ts index 9df61a5..0854ea7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -359,6 +359,8 @@ export const api = { req<{ ok: true; sent: number }>('POST', `/api/hotels/${slug}/workstations/update-all`), listPorts: (slug: string, wsId: string) => req<{ ports: { port: string; description?: string }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/ports`), + listPrinters: (slug: string, wsId: string) => + req<{ printers: { name: string; isDefault: boolean }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/printers`), }, // ── Agents ──────────────────────────────────────────────────────────────── @@ -1001,7 +1003,7 @@ export interface WorkstationDevice { workstationId: string type: 'kkt' | 'printer' | 'netup' name: string - connection: 'usb' | 'network' | 'com' + connection: 'usb' | 'network' | 'com' | 'windows' networkHost?: string networkPort?: number comPort?: string diff --git a/src/pages/EquipmentPage.tsx b/src/pages/EquipmentPage.tsx index ecf8d76..d4a953f 100644 --- a/src/pages/EquipmentPage.tsx +++ b/src/pages/EquipmentPage.tsx @@ -192,9 +192,10 @@ function DeviceForm({ const handleTypeChange = (t: 'kkt' | 'printer' | 'netup') => { setType(t) if (t === 'netup') setConnection('network') + if (t === 'kkt') setConnection('com') } const [name, setName] = useState(initial?.name ?? '') - const [connection, setConnection] = useState<'usb' | 'network' | 'com'>(initial?.connection ?? 'network') + const [connection, setConnection] = useState<'usb' | 'network' | 'com' | 'windows'>(initial?.connection ?? 'network') const [networkHost, setNetworkHost] = useState(initial?.networkHost ?? '') const [networkPort, setNetworkPort] = useState(String(initial?.networkPort ?? 9100)) const [comPort, setComPort] = useState(initial?.comPort ?? '') @@ -205,33 +206,70 @@ function DeviceForm({ const [dtoPass, setDtoPass] = useState(String(initial?.config?.dto_pass ?? '30')) const [dtoDeviceId, setDtoDeviceId] = useState(String(initial?.config?.dto_device_id ?? '')) const [saving, setSaving] = useState(false) - const [availablePorts, setAvailablePorts] = useState<{ port: string; description?: string }[] | null>(null) - const [portsLoading, setPortsLoading] = useState(false) + const [availablePorts, setAvailablePorts] = useState<{ port: string; description?: string }[] | null>(null) + const [portsLoading, setPortsLoading] = useState(false) + const [availablePrinters, setAvailablePrinters] = useState<{ name: string; isDefault: boolean }[] | null>(null) + const [printersLoading, setPrintersLoading] = useState(false) + const [windowsPrinter, setWindowsPrinter] = useState(String(initial?.config?.windows_printer ?? '')) - // Загружаем порты когда выбирается COM - useEffect(() => { - if (connection !== 'com' || !wsOnline || availablePorts !== null) return + const loadPorts = () => { + if (!wsOnline) return + setAvailablePorts(null) setPortsLoading(true) api.workstations.listPorts(slug, wsId) .then(r => setAvailablePorts(r.ports)) .catch(() => setAvailablePorts([])) .finally(() => setPortsLoading(false)) - }, [connection, wsOnline, slug, wsId, availablePorts]) + } + + const loadPrinters = () => { + if (!wsOnline) return + setAvailablePrinters(null) + setPrintersLoading(true) + api.workstations.listPrinters(slug, wsId) + .then(r => setAvailablePrinters(r.printers)) + .catch(() => setAvailablePrinters([])) + .finally(() => setPrintersLoading(false)) + } + + // Загружаем порты когда выбирается COM + useEffect(() => { + if (connection !== 'com' || !wsOnline || availablePorts !== null) return + loadPorts() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connection, wsOnline]) + + // Загружаем принтеры когда выбирается Windows + useEffect(() => { + if (connection !== 'windows' || !wsOnline || availablePrinters !== null) return + loadPrinters() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connection, wsOnline]) const handleSave = async () => { if (!name.trim()) return setSaving(true) + const configData: Record = {} + if (type === 'kkt' && fiscalMode) { + configData.dto_user = dtoUser + configData.dto_pass = dtoPass + if (dtoDeviceId.trim()) configData.dto_device_id = dtoDeviceId.trim() + } + if (type === 'printer' && connection === 'windows' && windowsPrinter.trim()) { + configData.windows_printer = windowsPrinter.trim() + } + await onSave({ type, name: name.trim(), - connection, + connection: type === 'kkt' ? 'com' : connection, networkHost: connection === 'network' ? networkHost.trim() : undefined, networkPort: connection === 'network' ? Number(networkPort) : undefined, comPort: connection === 'com' ? comPort.trim() : undefined, purpose: purpose as WorkstationDevice['purpose'], fiscalMode: type === 'kkt' ? fiscalMode : undefined, dtoPort: type === 'kkt' && fiscalMode ? Number(dtoPort) : undefined, - config: type === 'kkt' && fiscalMode ? { dto_user: dtoUser, dto_pass: dtoPass, dto_device_id: dtoDeviceId.trim() || undefined } : undefined, + config: Object.keys(configData).length ? configData : undefined, }) setSaving(false) } @@ -354,14 +392,15 @@ function DeviceForm({ /> - {type !== 'netup' && ( + {type === 'printer' && (
-
+
{([ + { id: 'network', icon: , label: 'Сеть (TCP)' }, + { id: 'windows', icon: , label: 'Windows принтер' }, + { id: 'com', icon: , label: 'COM-порт' }, { id: 'usb', icon: , label: 'USB' }, - { id: 'network', icon: , label: 'Сеть (TCP)' }, - { id: 'com', icon: , label: 'COM-порт' }, ] as const).map(c => ( + )} + + {wsOnline && availablePrinters && availablePrinters.length > 0 ? ( + + ) : ( + setWindowsPrinter(e.target.value)} + className="w-full input text-sm" + placeholder="Microsoft Print to PDF" + /> + )} + {wsOnline && availablePrinters !== null && availablePrinters.length === 0 && ( +

Принтеры не найдены. Убедитесь что принтер установлен в Windows.

+ )} + {!wsOnline && ( +

Терминал офлайн — введите имя принтера вручную

+ )} +
+ )} + {connection === 'com' && (
-