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:
2026-03-27 12:41:31 +03:00
parent c5413566dd
commit b2b2034f4c
4 changed files with 51 additions and 19 deletions

View File

@@ -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>
}

View File

@@ -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>