import { useState, useEffect, useCallback, useRef } from 'react' import { CreditCard, Banknote, LogIn, LogOut, BarChart2, Receipt, Coffee, Car, Package, Zap, Building2, RefreshCw, AlertCircle, CheckCircle2, Clock, Plus, Minus, Trash2, User, Printer, ArrowLeft, TrendingUp, DollarSign, WifiOff, } from 'lucide-react' import { format } from 'date-fns' import { ru } from 'date-fns/locale' import { cn, formatCurrency } from '../lib/utils' import { useAuth } from '../contexts/AuthContext' import { getIdentity, kkt, type AgentIdentity, type KktStatus, type ShiftStatus, type CashierInfo, type ReceiptData, type ReceiptItem, } from '../lib/agent' // ── Types ────────────────────────────────────────────────────────────────────── type PaymentType = 'cash' | 'card' type PosView = 'main' | 'payment' | 'cash-io' interface CartItem { name: string price: number quantity: number vat: 'none' | 'vat0' | 'vat10' | 'vat20' } interface GuestRow { roomNumber: string guestName: string balance: number // сумма к оплате bookingId: string } // ── Быстрые услуги ───────────────────────────────────────────────────────────── const QUICK_SERVICES = [ { name: 'Проживание', price: 0, icon: Building2, vat: 'none' as const, askPrice: true }, { name: 'Завтрак', price: 750, icon: Coffee, vat: 'none' as const, askPrice: false }, { name: 'Трансфер', price: 2500, icon: Car, vat: 'none' as const, askPrice: false }, { name: 'Парковка/сутки',price: 500, icon: Package, vat: 'none' as const, askPrice: false }, { name: 'Доп. услуга', price: 0, icon: Zap, vat: 'none' as const, askPrice: true }, ] // ── Helpers ──────────────────────────────────────────────────────────────────── function shiftDuration(openedAt: string): string { const diff = Date.now() - new Date(openedAt).getTime() const h = Math.floor(diff / 3600000) const m = Math.floor((diff % 3600000) / 60000) return h > 0 ? `${h} ч ${m} мин` : `${m} мин` } // ── Sub-components ───────────────────────────────────────────────────────────── function AgentOffline() { return (

Агент не подключён

На этом компьютере не запущен HotelSync Agent.
Обратитесь к системному администратору.

) } function ShiftClosed({ onOpen, loading }: { onOpen: () => void; loading: boolean }) { return (

Смена закрыта

Откройте смену чтобы начать принимать оплату

) } function ShiftExpired({ onClose, loading }: { onClose: () => void; loading: boolean }) { return (

Смена истекла (более 24 часов)

Необходимо закрыть текущую смену и открыть новую

) } // ── Шапка с информацией о смене ──────────────────────────────────────────────── function ShiftHeader({ identity, shift, kktStatus, onXReport, onCloseShift, onCashIn, onCashOut, onRefresh, loadingAction, }: { identity: AgentIdentity shift: ShiftStatus kktStatus: KktStatus onXReport: () => void onCloseShift: () => void onCashIn: () => void onCashOut: () => void onRefresh: () => void loadingAction: string | null }) { const ofdColor = kktStatus.info?.ofd_status === 'ok' ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500' return (
{/* Рабочее место */}
Рабочее место: {identity.workstation_name}
{/* Смена */}
Смена №{shift.shift_number}: {shift.opened_at ? shiftDuration(shift.opened_at) : '—'}
{/* Наличные */} {shift.cash_sum !== undefined && (
В кассе: {formatCurrency(shift.cash_sum)}
)} {/* ОФД статус */} {kktStatus.info && (
ОФД: {kktStatus.info.ofd_status === 'ok' ? '✓ Подключён' : '✗ Нет связи'}
)} {/* Кнопки */}
) } // ── Диалог внесения/изъятия наличных ────────────────────────────────────────── function CashDialog({ type, cashier, onConfirm, onClose, }: { type: 'in' | 'out' cashier: CashierInfo onConfirm: (amount: number) => Promise onClose: () => void }) { const [amount, setAmount] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const submit = async () => { const val = parseFloat(amount.replace(',', '.')) if (!val || val <= 0) { setError('Введите сумму'); return } setLoading(true) setError(null) try { await onConfirm(val) onClose() } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка') } finally { setLoading(false) } } return (

{type === 'in' ? '💵 Внесение наличных' : '💴 Изъятие наличных'}

setAmount(e.target.value)} onKeyDown={e => e.key === 'Enter' && submit()} className="w-full input-field text-xl font-mono text-center mb-2" placeholder="0.00" autoFocus type="number" min="0" /> {error &&

{error}

}
) } // ── Панель чека ──────────────────────────────────────────────────────────────── function CartPanel({ cart, guest, paymentType, onSetPayment, onRemoveItem, onChangeQty, onPay, paying, payResult, onBack, }: { cart: CartItem[] guest: GuestRow | null paymentType: PaymentType onSetPayment: (t: PaymentType) => void onRemoveItem: (i: number) => void onChangeQty: (i: number, d: number) => void onPay: () => void paying: boolean payResult: { ok: boolean; receipt_number?: string; error?: string } | null onBack: () => void }) { const total = cart.reduce((s, i) => s + i.price * i.quantity, 0) if (payResult?.ok) { return (

Оплата принята

{formatCurrency(total)}

{payResult.receipt_number && (

Чек №{payResult.receipt_number}

)}
) } return (
{/* Гость */} {guest && (
№{guest.roomNumber} — {guest.guestName}
)} {/* Позиции */}
{cart.length === 0 ? (

Выберите услугу из списка слева

) : ( cart.map((item, i) => (

{item.name}

{formatCurrency(item.price)} × {item.quantity}

{item.quantity}

{formatCurrency(item.price * item.quantity)}

)) )}
{/* Итого и оплата */} {cart.length > 0 && (
Итого {formatCurrency(total)}
{/* Способ оплаты */}
{([['cash', 'Наличные', Banknote], ['card', 'Терминал', CreditCard]] as const).map(([t, label, Icon]) => ( ))}
{payResult?.error && (

{payResult.error}

)}
)}
) } // ── Main Page ────────────────────────────────────────────────────────────────── export function PosPage() { const { user } = useAuth() // ── State ────────────────────────────────────────────────────────────────── const [agentOnline, setAgentOnline] = useState(null) // null = loading const [identity, setIdentity] = useState(null) const [kktStatus, setKktStatus] = useState(null) const [loadingAction, setLoadingAction] = useState(null) const [actionError, setActionError] = useState(null) const [actionSuccess, setActionSuccess] = useState(null) const [cart, setCart] = useState([]) const [selectedGuest, setGuest] = useState(null) const [paymentType, setPaymentType] = useState('cash') const [paying, setPaying] = useState(false) const [payResult, setPayResult] = useState<{ ok: boolean; receipt_number?: string; error?: string } | null>(null) const [cashDialog, setCashDialog] = useState<'in' | 'out' | null>(null) const [customService, setCustomService] = useState<{ name: string; price: string } | null>(null) const pollRef = useRef | null>(null) // Временные гости (mock — в будущем из API) const [guests] = useState([ { roomNumber: '101', guestName: 'Дмитрий Волков', balance: 4500, bookingId: 'b1' }, { roomNumber: '301', guestName: 'Наталья Александрова', balance: 24000, bookingId: 'b2' }, { roomNumber: '401', guestName: 'Михаил Орлов', balance: 36000, bookingId: 'b3' }, ]) // ── Кассир (из профиля) ──────────────────────────────────────────────────── const cashier: CashierInfo = { name: user?.name ?? 'Кассир' } // ── Загрузка статуса ─────────────────────────────────────────────────────── const loadStatus = useCallback(async () => { const id = await getIdentity() if (!id) { setAgentOnline(false); return } setAgentOnline(true) setIdentity(id) try { const status = await kkt.status() setKktStatus(status) } catch { setKktStatus(null) } }, []) useEffect(() => { loadStatus() pollRef.current = setInterval(loadStatus, 15000) return () => { if (pollRef.current) clearInterval(pollRef.current) } }, [loadStatus]) // ── Кассовые действия ────────────────────────────────────────────────────── const doAction = async (key: string, fn: () => Promise<{ ok: boolean; error?: string }>, successMsg: string) => { setLoadingAction(key) setActionError(null) setActionSuccess(null) const result = await fn() setLoadingAction(null) if (result.ok) { setActionSuccess(successMsg) setTimeout(() => setActionSuccess(null), 3000) await loadStatus() } else { setActionError(result.error ?? 'Ошибка') } } const handleOpenShift = () => doAction('open', () => kkt.openShift(cashier), 'Смена открыта') const handleCloseShift = () => doAction('close', () => kkt.closeShift(cashier), 'Смена закрыта, Z-отчёт распечатан') const handleXReport = () => doAction('xreport',() => kkt.xReport(), 'X-отчёт распечатан') const handleCashIn = async (amount: number) => { const r = await kkt.cashIn(amount, cashier) if (!r.ok) throw new Error(r.error) setActionSuccess(`Внесено ${formatCurrency(amount)}`) setTimeout(() => setActionSuccess(null), 3000) await loadStatus() } const handleCashOut = async (amount: number) => { const r = await kkt.cashOut(amount, cashier) if (!r.ok) throw new Error(r.error) setActionSuccess(`Изъято ${formatCurrency(amount)}`) setTimeout(() => setActionSuccess(null), 3000) await loadStatus() } // ── Чек ─────────────────────────────────────────────────────────────────── const addService = (name: string, price: number) => { setCart(prev => { const existing = prev.findIndex(i => i.name === name) if (existing >= 0) { const next = [...prev] next[existing] = { ...next[existing], quantity: next[existing].quantity + 1 } return next } return [...prev, { name, price, quantity: 1, vat: 'none' }] }) setPayResult(null) } const changeQty = (idx: number, delta: number) => { setCart(prev => { const next = [...prev] const q = next[idx].quantity + delta if (q <= 0) return next.filter((_, i) => i !== idx) next[idx] = { ...next[idx], quantity: q } return next }) } const handlePay = async () => { if (!cart.length) return setPaying(true) setPayResult(null) const data: ReceiptData = { type: 'sell', items: cart.map(i => ({ name: i.name, quantity: i.quantity, price: i.price, vat: i.vat, payment_object: 'service' })), total: cart.reduce((s, i) => s + i.price * i.quantity, 0), payment_type: paymentType, cashier, } const result = await kkt.printReceipt(data) setPayResult(result) setPaying(false) if (result.ok) { setCart([]) setGuest(null) } } const resetCart = () => { setCart([]); setGuest(null); setPayResult(null) } // ── Render ───────────────────────────────────────────────────────────────── if (agentOnline === null) { return (
) } if (!agentOnline) return const shift = kktStatus?.shift if (!shift || shift.state === 'closed') { return (
{actionError && (
{actionError}
)}
) } if (shift.state === 'expired') { return (
) } // ── Смена открыта — основной интерфейс ──────────────────────────────────── return (
{/* Шапка смены */} setCashDialog('in')} onCashOut={() => setCashDialog('out')} onRefresh={loadStatus} loadingAction={loadingAction} /> {/* Уведомления */} {(actionError || actionSuccess) && (
{actionError ? : } {actionError || actionSuccess}
)} {/* Основная зона */}
{/* Левая панель — гости и услуги */}
{/* Гости */}

Гости в отеле

{guests.map(g => ( ))}
{/* Услуги */}

Быстрые услуги

{QUICK_SERVICES.map(s => ( ))} {/* Произвольная сумма */}
{/* Правая панель — чек */}
setCart(prev => prev.filter((_, idx) => idx !== i))} onChangeQty={changeQty} onPay={handlePay} paying={paying} payResult={payResult} onBack={resetCart} />
{/* Диалог произвольной услуги */} {customService && (

Добавить услугу

setCustomService(s => s && { ...s, name: e.target.value })} className="w-full input-field mb-3" /> setCustomService(s => s && { ...s, price: e.target.value })} onKeyDown={e => { if (e.key === 'Enter') { const p = parseFloat(customService.price.replace(',', '.')) if (p > 0 && customService.name.trim()) { addService(customService.name.trim(), p) setCustomService(null) } } }} className="w-full input-field text-xl font-mono text-center mb-4" placeholder="0.00" type="number" autoFocus />
)} {/* Диалог наличных */} {cashDialog && ( setCashDialog(null)} /> )}
) }