feat: касса — полная переработка PosPage + интеграция с агентом

- PosPage.tsx: полный редизайн вокруг реальной кассовой работы
  • Автодетект агента (localhost:9000), статус смены каждые 15с
  • Экраны: агент не подключён / смена закрыта / смена истекла / касса
  • Шапка смены: №, длительность, остаток наличных, статус ОФД
  • Кнопки: X-отчёт, Внести/Изъять наличные, Закрыть смену
  • Левая панель: гости с балансом + быстрые услуги
  • Правая панель: чек с количеством, итого, выбор наличные/карта
  • Экран успеха после пробития чека с номером ФД
- agent.ts: клиент localhost:9000 — identity + все kkt операции
- index.css: btn-danger класс

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 18:55:12 +03:00
parent b5974ff97f
commit fbb06265f9
3 changed files with 894 additions and 875 deletions

View File

@@ -82,6 +82,14 @@
disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-danger {
@apply inline-flex items-center gap-2 px-4 py-2 rounded-lg
bg-red-600 hover:bg-red-700
text-white text-sm font-medium
transition-colors duration-150
disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-secondary {
@apply inline-flex items-center gap-2 px-4 py-2 rounded-lg
bg-white dark:bg-slate-800

129
src/lib/agent.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* Клиент для общения с HotelSync Agent на localhost:9000
* Все кассовые операции идут напрямую через агент, минуя сервер
*/
const AGENT_URL = 'http://localhost:9000'
const TIMEOUT = 30000 // 30 секунд для кассовых операций
async function agentReq<T>(
method: 'GET' | 'POST',
path: string,
body?: object,
timeout = TIMEOUT,
): Promise<T> {
const res = await fetch(`${AGENT_URL}${path}`, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(timeout),
})
return res.json() as Promise<T>
}
// ── Типы ──────────────────────────────────────────────────────────────────────
export interface AgentIdentity {
agent_id: string
workstation_id: string
workstation_name: string
hostname: string
error?: string
}
export interface ShiftStatus {
state: 'closed' | 'open' | 'expired'
shift_number: number
opened_at?: string
duration_min?: number
cash_sum?: number
receipt_count?: number
cashier_name?: string
}
export interface KktInfo {
serial_number: string
model: string
fn_number: string
fn_expires_at?: string
ofd_status: 'ok' | 'error' | 'no_connection'
version: string
}
export interface KktStatus {
shift?: ShiftStatus
info?: KktInfo
dto_connected: boolean
dto_version?: string
}
export interface CashierInfo {
name: string
inn?: string
}
export interface ReceiptItem {
name: string
quantity: number
price: number
vat?: 'none' | 'vat0' | 'vat10' | 'vat20'
payment_object?: 'commodity' | 'service'
}
export interface ReceiptData {
type: 'sell' | 'sell_return'
items: ReceiptItem[]
total: number
payment_type: 'cash' | 'card' | 'prepaid'
cashier?: CashierInfo
customer_email?: string
customer_phone?: string
tax_system?: number
}
export interface AgentResult {
ok: boolean
error?: string
receipt_number?: string
fiscal_sign?: string
shift_number?: number
}
// ── API ───────────────────────────────────────────────────────────────────────
/** Проверяет что агент запущен и возвращает рабочее место. Timeout 500ms. */
export async function getIdentity(): Promise<AgentIdentity | null> {
try {
const data = await agentReq<AgentIdentity>('GET', '/identity', undefined, 500)
if (data.error) return null
return data
} catch {
return null
}
}
export const kkt = {
status: () =>
agentReq<KktStatus>('GET', '/kkt/status', undefined, 5000),
openShift: (cashier: CashierInfo, tax_system = 1) =>
agentReq<AgentResult>('POST', '/kkt/shift/open', { cashier, tax_system }),
closeShift: (cashier: CashierInfo) =>
agentReq<AgentResult>('POST', '/kkt/shift/close', { cashier }),
xReport: () =>
agentReq<AgentResult>('POST', '/kkt/report/x', {}),
cashIn: (amount: number, cashier: CashierInfo) =>
agentReq<AgentResult>('POST', '/kkt/cash-in', { amount, cashier }),
cashOut: (amount: number, cashier: CashierInfo) =>
agentReq<AgentResult>('POST', '/kkt/cash-out', { amount, cashier }),
printReceipt: (data: ReceiptData) =>
agentReq<AgentResult>('POST', '/kkt/receipt', data),
printReturn: (data: ReceiptData) =>
agentReq<AgentResult>('POST', '/kkt/return', data),
}

File diff suppressed because it is too large Load Diff