From f882cb24815b06ccd194ec386646580a9619da53 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Fri, 27 Mar 2026 14:06:44 +0300 Subject: [PATCH] feat: KKT fiscal/non-fiscal mode, multi-KKT per workstation, DTO port per device - Migration 045: fiscal_mode + dto_port columns on workstation_devices - Backend: pass fiscal_mode/dto_port to agent in test_device command - Agent atol.ts: remove global DTO singleton, accept dtoPort per function call - Agent test_device: returns real KKT info (model, serial, FN, shift status) - Equipment page: fiscal/non-fiscal toggle + DTO port field when adding KKT - Equipment page: show fiscal mode badge on device card, DTO port if non-default - Equipment page: KKT test result shows model, serial, FN, shift status Co-Authored-By: Claude Sonnet 4.6 --- backend/migrations/045_kkt_config.sql | 4 ++ backend/src/routes/workstations.ts | 14 +++-- src/lib/api.ts | 2 + src/pages/EquipmentPage.tsx | 75 +++++++++++++++++++++++++-- 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 backend/migrations/045_kkt_config.sql diff --git a/backend/migrations/045_kkt_config.sql b/backend/migrations/045_kkt_config.sql new file mode 100644 index 0000000..2c441d7 --- /dev/null +++ b/backend/migrations/045_kkt_config.sql @@ -0,0 +1,4 @@ +-- KKT fiscal mode and DTO port per device +ALTER TABLE workstation_devices + ADD COLUMN IF NOT EXISTS fiscal_mode BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS dto_port INTEGER DEFAULT 16732; diff --git a/backend/src/routes/workstations.ts b/backend/src/routes/workstations.ts index 83730e9..e7b7ed2 100644 --- a/backend/src/routes/workstations.ts +++ b/backend/src/routes/workstations.ts @@ -211,16 +211,18 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => { fastify.post( '/api/hotels/:slug/workstations/:id/devices', { onRequest: [fastify.authenticate] }, async (req, reply) => { const { id } = req.params - const { type, name, connection, network_host, network_port, com_port, purpose, config } = req.body + const { type, name, connection, network_host, network_port, com_port, purpose, config, fiscal_mode, dto_port } = req.body const { rows } = await db.query( - `INSERT INTO workstation_devices (workstation_id, type, name, connection, network_host, network_port, com_port, purpose, config) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`, - [id, type, name, connection, network_host ?? null, network_port ?? null, com_port ?? null, purpose ?? 'other', config ?? {}], + `INSERT INTO workstation_devices (workstation_id, type, name, connection, network_host, network_port, com_port, purpose, config, fiscal_mode, dto_port) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING *`, + [id, type, name, connection, network_host ?? null, network_port ?? null, com_port ?? null, purpose ?? 'other', config ?? {}, + fiscal_mode ?? true, dto_port ?? 16732], ) return reply.code(201).send(rows[0]) }, @@ -233,7 +235,7 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => { async (req, reply) => { const { deviceId } = req.params const body = req.body as Record - const fields = ['name', 'connection', 'network_host', 'network_port', 'com_port', 'purpose', 'config'] + const fields = ['name', 'connection', 'network_host', 'network_port', 'com_port', 'purpose', 'config', 'fiscal_mode', 'dto_port'] const sets: string[] = [] const vals: unknown[] = [] fields.forEach(f => { @@ -296,6 +298,8 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => { network_host: device.network_host, network_port: device.network_port, com_port: device.com_port, + fiscal_mode: device.fiscal_mode, + dto_port: device.dto_port, }, }) as { ok: boolean; error?: string; message?: string } return result diff --git a/src/lib/api.ts b/src/lib/api.ts index 19bc7a3..e58cf63 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1007,6 +1007,8 @@ export interface WorkstationDevice { comPort?: string purpose: 'fiscal' | 'kitchen' | 'bar' | 'receipt' | 'other' config: Record + fiscalMode?: boolean + dtoPort?: number } export interface Workstation { diff --git a/src/pages/EquipmentPage.tsx b/src/pages/EquipmentPage.tsx index 016fa9f..addda60 100644 --- a/src/pages/EquipmentPage.tsx +++ b/src/pages/EquipmentPage.tsx @@ -195,6 +195,8 @@ function DeviceForm({ const [networkPort, setNetworkPort] = useState(String(initial?.networkPort ?? 9100)) const [comPort, setComPort] = useState(initial?.comPort ?? '') const [purpose, setPurpose] = useState(initial?.purpose ?? 'receipt') + const [fiscalMode, setFiscalMode] = useState(initial?.fiscalMode !== false) + const [dtoPort, setDtoPort] = useState(String(initial?.dtoPort ?? 16732)) const [saving, setSaving] = useState(false) const [availablePorts, setAvailablePorts] = useState<{ port: string; description?: string }[] | null>(null) const [portsLoading, setPortsLoading] = useState(false) @@ -220,6 +222,8 @@ function DeviceForm({ 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, }) setSaving(false) } @@ -253,6 +257,48 @@ function DeviceForm({ + {/* KKT-specific: fiscal mode + DTO port */} + {type === 'kkt' && ( +
+
+ +
+ {([ + { id: true, label: 'Фискальный', hint: 'Чеки с ФН, ОФД' }, + { id: false, label: 'Нефискальный', hint: 'Только документы' }, + ] as const).map(m => ( + + ))} +
+
+ {fiscalMode && ( +
+ + setDtoPort(e.target.value)} + className="w-32 input text-sm font-mono" + placeholder="16732" + /> +

Порт АТОЛ ДТО 10 (по умолчанию 16732). Для нескольких касс — разные порты.

+
+ )} +
+ )} +
({ ...prev, [deviceId]: { ok: r.ok, msg: r.message ?? r.error ?? '' } })) + let msg = r.message ?? r.error ?? '' + // Для ккт — форматируем информацию о кассе + if (r.ok && r.kkt) { + const k = r.kkt as { model?: string; serial_number?: string; fn_number?: string; ofd_status?: string } + const s = r.shift as { state?: string; shift_number?: number } | undefined + const shiftLabel = s?.state === 'open' ? `Смена #${s.shift_number} открыта` : s?.state === 'expired' ? 'Смена истекла!' : 'Смена закрыта' + msg = `${k.model ?? 'ККТ'} · СН: ${k.serial_number ?? '—'} · ФН: ${k.fn_number ?? '—'} · ${shiftLabel}` + } + setTestResults(prev => ({ ...prev, [deviceId]: { ok: r.ok, msg } })) } catch (err) { setTestResults(prev => ({ ...prev, [deviceId]: { ok: false, msg: err instanceof Error ? err.message : 'Ошибка' } })) } finally { setTestingDeviceId(null) - setTimeout(() => setTestResults(prev => ({ ...prev, [deviceId]: null })), 4000) + setTimeout(() => setTestResults(prev => ({ ...prev, [deviceId]: null })), 8000) } } @@ -577,7 +631,19 @@ function WorkstationCard({
-

{device.name}

+
+

{device.name}

+ {device.type === 'kkt' && ( + + {device.fiscalMode !== false ? 'Фискальный' : 'Нефискальный'} + + )} +
{PURPOSE_LABELS[device.purpose]} · @@ -591,6 +657,9 @@ function WorkstationCard({ ? `${device.networkHost}:${device.networkPort}` : 'IP не задан'} + {device.type === 'kkt' && device.fiscalMode !== false && device.dtoPort && device.dtoPort !== 16732 && ( + DTO:{device.dtoPort} + )}