feat: COM-port support for workstation devices + agent UI fixes
- EquipmentPage: add COM-port connection type with port field (COM1, /dev/ttyUSB0) - api.ts: add comPort field to WorkstationDevice type - workstations route: pass com_port to INSERT/PATCH - migration 042: extend connection check constraint to include 'com', add com_port column - agent index.html: highlight first cell on load, reset input on unpair Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
13
backend/migrations/042_workstation_devices_com.sql
Normal file
13
backend/migrations/042_workstation_devices_com.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Добавляем поддержку COM-порта для устройств рабочего места
|
||||
|
||||
-- Расширяем CHECK constraint для connection (добавляем 'com')
|
||||
ALTER TABLE workstation_devices
|
||||
DROP CONSTRAINT IF EXISTS workstation_devices_connection_check;
|
||||
|
||||
ALTER TABLE workstation_devices
|
||||
ADD CONSTRAINT workstation_devices_connection_check
|
||||
CHECK (connection IN ('usb', 'network', 'com'));
|
||||
|
||||
-- Добавляем поле com_port (например: COM1, COM3, /dev/ttyUSB0)
|
||||
ALTER TABLE workstation_devices
|
||||
ADD COLUMN IF NOT EXISTS com_port VARCHAR(50);
|
||||
@@ -209,17 +209,17 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
// ── POST /api/hotels/:slug/workstations/:id/devices ─────────────────────────
|
||||
fastify.post<WsParam & { Body: {
|
||||
type: string; name: string; connection: string
|
||||
network_host?: string; network_port?: number; purpose?: string; config?: object
|
||||
network_host?: string; network_port?: number; com_port?: string; purpose?: string; config?: object
|
||||
} }>(
|
||||
'/api/hotels/:slug/workstations/:id/devices',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (req, reply) => {
|
||||
const { id } = req.params
|
||||
const { type, name, connection, network_host, network_port, purpose, config } = req.body
|
||||
const { type, name, connection, network_host, network_port, com_port, purpose, config } = req.body
|
||||
const { rows } = await db.query(
|
||||
`INSERT INTO workstation_devices (workstation_id, type, name, connection, network_host, network_port, purpose, config)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
||||
[id, type, name, connection, network_host ?? null, network_port ?? null, purpose ?? 'other', config ?? {}],
|
||||
`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 ?? {}],
|
||||
)
|
||||
return reply.code(201).send(rows[0])
|
||||
},
|
||||
@@ -232,7 +232,7 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
async (req, reply) => {
|
||||
const { deviceId } = req.params
|
||||
const body = req.body as Record<string, unknown>
|
||||
const fields = ['name', 'connection', 'network_host', 'network_port', 'purpose', 'config']
|
||||
const fields = ['name', 'connection', 'network_host', 'network_port', 'com_port', 'purpose', 'config']
|
||||
const sets: string[] = []
|
||||
const vals: unknown[] = []
|
||||
fields.forEach(f => {
|
||||
|
||||
@@ -972,9 +972,10 @@ export interface WorkstationDevice {
|
||||
workstationId: string
|
||||
type: 'kkt' | 'printer'
|
||||
name: string
|
||||
connection: 'usb' | 'network'
|
||||
connection: 'usb' | 'network' | 'com'
|
||||
networkHost?: string
|
||||
networkPort?: number
|
||||
comPort?: string
|
||||
purpose: 'fiscal' | 'kitchen' | 'bar' | 'receipt' | 'other'
|
||||
config: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Monitor, Plus, Trash2, Pencil, Check, X, RefreshCw,
|
||||
Wifi, WifiOff, Printer, CreditCard, Usb, Network,
|
||||
Wifi, WifiOff, Printer, CreditCard, Usb, Network, Cable,
|
||||
ChevronDown, ChevronRight, Copy, Clock, AlertCircle,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '../lib/utils'
|
||||
@@ -171,9 +171,10 @@ function DeviceForm({
|
||||
}) {
|
||||
const [type, setType] = useState<'kkt' | 'printer'>(initial?.type ?? 'printer')
|
||||
const [name, setName] = useState(initial?.name ?? '')
|
||||
const [connection, setConnection] = useState<'usb' | 'network'>(initial?.connection ?? 'network')
|
||||
const [connection, setConnection] = useState<'usb' | 'network' | 'com'>(initial?.connection ?? 'network')
|
||||
const [networkHost, setNetworkHost] = useState(initial?.networkHost ?? '')
|
||||
const [networkPort, setNetworkPort] = useState(String(initial?.networkPort ?? 9100))
|
||||
const [comPort, setComPort] = useState(initial?.comPort ?? 'COM1')
|
||||
const [purpose, setPurpose] = useState(initial?.purpose ?? 'receipt')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
@@ -184,8 +185,9 @@ function DeviceForm({
|
||||
type,
|
||||
name: name.trim(),
|
||||
connection,
|
||||
networkHost: connection === 'network' ? networkHost.trim() : undefined,
|
||||
networkPort: connection === 'network' ? Number(networkPort) : undefined,
|
||||
networkHost: connection === 'network' ? networkHost.trim() : undefined,
|
||||
networkPort: connection === 'network' ? Number(networkPort) : undefined,
|
||||
comPort: connection === 'com' ? comPort.trim() : undefined,
|
||||
purpose: purpose as WorkstationDevice['purpose'],
|
||||
})
|
||||
setSaving(false)
|
||||
@@ -232,19 +234,22 @@ function DeviceForm({
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">Подключение</label>
|
||||
<div className="flex gap-2">
|
||||
{(['usb', 'network'] as const).map(c => (
|
||||
{([
|
||||
{ id: 'usb', icon: <Usb size={14} />, label: 'USB' },
|
||||
{ id: 'network', icon: <Network size={14} />, label: 'Сеть (TCP)' },
|
||||
{ id: 'com', icon: <Cable size={14} />, label: 'COM-порт' },
|
||||
] as const).map(c => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setConnection(c)}
|
||||
key={c.id}
|
||||
onClick={() => setConnection(c.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors',
|
||||
connection === c
|
||||
connection === c.id
|
||||
? 'bg-brand-600 border-brand-600 text-white'
|
||||
: 'border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:border-brand-400',
|
||||
)}
|
||||
>
|
||||
{c === 'usb' ? <Usb size={14} /> : <Network size={14} />}
|
||||
{c === 'usb' ? 'USB' : 'Сеть (TCP)'}
|
||||
{c.icon}{c.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -273,6 +278,19 @@ function DeviceForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connection === 'com' && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">COM-порт</label>
|
||||
<input
|
||||
value={comPort}
|
||||
onChange={e => setComPort(e.target.value)}
|
||||
className="w-full input text-sm font-mono"
|
||||
placeholder="COM1"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">Windows: COM1, COM3 и т.д. Linux: /dev/ttyUSB0</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
@@ -441,8 +459,8 @@ function WorkstationCard({
|
||||
<span className="text-xs text-slate-500">{PURPOSE_LABELS[device.purpose]}</span>
|
||||
<span className="text-slate-300 dark:text-slate-600">·</span>
|
||||
<span className="inline-flex items-center gap-1 text-xs text-slate-500">
|
||||
{device.connection === 'usb' ? <Usb size={10} /> : <Network size={10} />}
|
||||
{device.connection === 'usb' ? 'USB' : `${device.networkHost}:${device.networkPort}`}
|
||||
{device.connection === 'usb' ? <Usb size={10} /> : device.connection === 'com' ? <Cable size={10} /> : <Network size={10} />}
|
||||
{device.connection === 'usb' ? 'USB' : device.connection === 'com' ? device.comPort : `${device.networkHost}:${device.networkPort}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user