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:
@@ -82,6 +82,14 @@
|
|||||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
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 {
|
.btn-secondary {
|
||||||
@apply inline-flex items-center gap-2 px-4 py-2 rounded-lg
|
@apply inline-flex items-center gap-2 px-4 py-2 rounded-lg
|
||||||
bg-white dark:bg-slate-800
|
bg-white dark:bg-slate-800
|
||||||
|
|||||||
129
src/lib/agent.ts
Normal file
129
src/lib/agent.ts
Normal 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
Reference in New Issue
Block a user