import { useState, useEffect } from 'react' import { CreditCard, Banknote, Clock, LogIn, LogOut, CheckCircle2, Receipt, Zap, Building2, Coffee, Car, Package, ChevronRight, User, Printer, BarChart2, Settings2, Wifi, WifiOff, FileText, RefreshCw, } from 'lucide-react' import { format } from 'date-fns' import { ru } from 'date-fns/locale' import { cn, formatCurrency } from '../lib/utils' // ── Types ───────────────────────────────────────────────────────────────────── interface RoomBalance { roomId: string roomNumber: string guestName: string checkIn: string checkOut: string totalAmount: number paidAmount: number nights: number } type PaymentMethod = 'cash' | 'terminal' type ChargePurpose = 'stay' | 'breakfast' | 'transfer' | 'parking' | 'extra' type PosTab = 'main' | 'report' | 'atol' type AtolConnection = 'usb' | 'tcp' | 'com' type TaxSystem = 'osn' | 'usn_income' | 'usn_expense' | 'eshn' | 'psn' interface ReceiptRecord { id: string roomNumber: string guestName: string purposeLabel: string amount: number method: PaymentMethod createdAt: Date shiftId: string } // ── Mock data ────────────────────────────────────────────────────────────────── const MOCK_BALANCES: RoomBalance[] = [ { roomId: 'r1', roomNumber: '101', guestName: 'Дмитрий Волков', checkIn: '2026-03-10', checkOut: '2026-03-12', totalAmount: 9000, paidAmount: 4500, nights: 2 }, { roomId: 'r2', roomNumber: '201', guestName: 'Игорь Соколов', checkIn: '2026-03-09', checkOut: '2026-03-11', totalAmount: 14000, paidAmount: 14000, nights: 2 }, { roomId: 'r3', roomNumber: '301', guestName: 'Наталья Александрова', checkIn: '2026-03-11', checkOut: '2026-03-15', totalAmount: 48000, paidAmount: 24000, nights: 4 }, { roomId: 'r4', roomNumber: '401', guestName: 'Михаил Орлов', checkIn: '2026-03-10', checkOut: '2026-03-13', totalAmount: 36000, paidAmount: 0, nights: 3 }, ] const PURPOSE_LABELS: Record = { stay: 'Проживание', breakfast: 'Завтрак', transfer: 'Трансфер', parking: 'Парковка', extra: 'Доп. услуга', } const QUICK_CHARGES: { purpose: ChargePurpose; label: string; amount: number | null; Icon: React.ElementType }[] = [ { purpose: 'stay', label: 'Проживание', amount: null, Icon: Building2 }, { purpose: 'breakfast', label: 'Завтрак', amount: 650, Icon: Coffee }, { purpose: 'transfer', label: 'Трансфер', amount: 2500, Icon: Car }, { purpose: 'parking', label: 'Парковка', amount: 500, Icon: Package }, { purpose: 'extra', label: 'Доп. услуга', amount: null, Icon: Zap }, ] const SEED_RECEIPTS: ReceiptRecord[] = [ { id: 'rc1', roomNumber: '201', guestName: 'Игорь Соколов', purposeLabel: 'Проживание', amount: 14000, method: 'terminal', createdAt: new Date(Date.now() - 3600000 * 3), shiftId: 'shift-today' }, { id: 'rc2', roomNumber: '101', guestName: 'Дмитрий Волков', purposeLabel: 'Проживание', amount: 4500, method: 'cash', createdAt: new Date(Date.now() - 3600000 * 2), shiftId: 'shift-today' }, { id: 'rc3', roomNumber: '301', guestName: 'Наталья Александрова', purposeLabel: 'Проживание', amount: 24000, method: 'terminal', createdAt: new Date(Date.now() - 3600000), shiftId: 'shift-today' }, { id: 'rc4', roomNumber: '101', guestName: 'Дмитрий Волков', purposeLabel: 'Завтрак', amount: 1300, method: 'cash', createdAt: new Date(Date.now() - 1800000), shiftId: 'shift-today' }, ] const TAX_SYSTEM_LABELS: Record = { osn: 'ОСН', usn_income: 'УСН доход', usn_expense:'УСН расход', eshn: 'ЕСХН', psn: 'ПСН', } // ── Subcomponents ───────────────────────────────────────────────────────────── function MethodIcon({ method, size = 11 }: { method: PaymentMethod; size?: number }) { if (method === 'cash') return return } // ── Component ───────────────────────────────────────────────────────────────── export function PosPage() { // Core state const [shiftOpen, setShiftOpen] = useState(true) const [shiftStartTime] = useState(new Date(Date.now() - 3600000 * 3)) const [shiftTimer, setShiftTimer] = useState('') const [receipts, setReceipts] = useState(SEED_RECEIPTS) const [selectedRoom, setSelectedRoom] = useState(MOCK_BALANCES[0]) const [purpose, setPurpose] = useState('stay') const [amount, setAmount] = useState(0) const [method, setMethod] = useState('terminal') const [notes, setNotes] = useState('') const [printFiscal, setPrintFiscal] = useState(true) const [lastReceipt, setLastReceipt] = useState(null) const [activeTab, setActiveTab] = useState('main') // Atol settings state const [atolConnected, setAtolConnected] = useState(false) const [atolChecking, setAtolChecking] = useState(false) const [atolConnType, setAtolConnType] = useState('usb') const [atolIp, setAtolIp] = useState('192.168.1.100') const [atolPort, setAtolPort] = useState('9100') const [atolComPort, setAtolComPort] = useState('COM1') const [atolBaud, setAtolBaud] = useState('115200') const [atolCashier, setAtolCashier] = useState('Елена Смирнова') const [atolTax, setAtolTax] = useState('osn') const [atolAutoClose, setAtolAutoClose] = useState(false) const [atolSaved, setAtolSaved] = useState(false) // Report state const [closeConfirm, setCloseConfirm] = useState(false) const shiftId = 'shift-today' // Update amount when room or purpose changes useEffect(() => { if (!selectedRoom) { setAmount(0); return } if (purpose === 'stay') { setAmount(Math.max(0, selectedRoom.totalAmount - selectedRoom.paidAmount)) } else { const qc = QUICK_CHARGES.find(q => q.purpose === purpose) setAmount(qc?.amount ?? 0) } }, [selectedRoom, purpose]) // Shift timer useEffect(() => { if (!shiftOpen) return const tick = () => { const diff = Math.floor((Date.now() - shiftStartTime.getTime()) / 1000) const h = Math.floor(diff / 3600) const m = Math.floor((diff % 3600) / 60) const s = diff % 60 setShiftTimer(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`) } tick() const id = setInterval(tick, 1000) return () => clearInterval(id) }, [shiftOpen, shiftStartTime]) const shiftReceipts = receipts.filter(r => r.shiftId === shiftId) const shiftRevenue = shiftReceipts.reduce((s, r) => s + r.amount, 0) const cashRevenue = shiftReceipts.filter(r => r.method === 'cash').reduce((s, r) => s + r.amount, 0) const termRevenue = shiftReceipts.filter(r => r.method === 'terminal').reduce((s, r) => s + r.amount, 0) const cashCount = shiftReceipts.filter(r => r.method === 'cash').length const termCount = shiftReceipts.filter(r => r.method === 'terminal').length const handlePay = () => { if (!selectedRoom || amount <= 0 || !shiftOpen) return const rec: ReceiptRecord = { id: `rc-${Date.now()}`, roomNumber: selectedRoom.roomNumber, guestName: selectedRoom.guestName, purposeLabel: PURPOSE_LABELS[purpose], amount, method, createdAt: new Date(), shiftId, } setReceipts(prev => [rec, ...prev]) setLastReceipt(rec) setNotes('') } const handleAtolCheck = () => { setAtolChecking(true) setTimeout(() => { setAtolChecking(false) setAtolConnected(true) }, 1000) } const handleAtolSave = () => { setAtolSaved(true) setTimeout(() => setAtolSaved(false), 2000) } const handleCloseShift = () => { setCloseConfirm(false) setShiftOpen(false) setActiveTab('main') } const balance = selectedRoom ? Math.max(0, selectedRoom.totalAmount - selectedRoom.paidAmount) : 0 const isPaid = selectedRoom ? selectedRoom.paidAmount >= selectedRoom.totalAmount : false const TABS: { key: PosTab; label: string; Icon: React.ElementType }[] = [ { key: 'main', label: 'Касса', Icon: Receipt }, { key: 'report', label: 'X/Z-отчёт', Icon: BarChart2 }, { key: 'atol', label: 'АТОЛ', Icon: Settings2 }, ] return (
{/* ── Desktop-only notice on mobile ── */}

Касса — только десктоп

Откройте приложение на компьютере или планшете для работы с кассой

{/* ── Full POS UI (desktop only) ── */}
{/* ── Top bar ── */}

Касса

{format(new Date(), 'd MMMM yyyy', { locale: ru })} · Елена Смирнова

{shiftOpen && (
{shiftTimer}
)}
{/* Tab bar */} {shiftOpen && (
{TABS.map(t => ( ))}
)}
{/* ── Shift closed screen ── */} {!shiftOpen ? (

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

Откройте смену для начала работы

) : ( <> {/* ══════════════════════ TAB: main ══════════════════════ */} {activeTab === 'main' && (
{/* ── Заезды: horizontal scroll on mobile, sidebar on desktop ── */}

Заезды

{/* Mobile: horizontal scroll row */}
{MOCK_BALANCES.map(rb => { const debt = Math.max(0, rb.totalAmount - rb.paidAmount) const paid = debt === 0 const sel = selectedRoom?.roomId === rb.roomId return ( ) })}
{/* Desktop: vertical list */}
{MOCK_BALANCES.map(rb => { const debt = Math.max(0, rb.totalAmount - rb.paidAmount) const paid = debt === 0 const sel = selectedRoom?.roomId === rb.roomId return ( ) })}
{/* ── Center: payment form ── */}
{selectedRoom ? ( <> {/* Guest info */}

{selectedRoom.guestName}

Номер {selectedRoom.roomNumber} · {selectedRoom.nights} {selectedRoom.nights === 1 ? 'ночь' : selectedRoom.nights < 5 ? 'ночи' : 'ночей'}

К оплате

{formatCurrency(balance)}

{/* Balance bar */}
Оплачено {formatCurrency(selectedRoom.paidAmount)} Всего {formatCurrency(selectedRoom.totalAmount)}
{/* Quick charge buttons */}

Назначение платежа

{QUICK_CHARGES.map(qc => ( ))}
{/* Amount */}
setAmount(Math.max(0, parseInt(e.target.value) || 0))} placeholder="0" />
{/* Payment method */}

Способ оплаты

{([ { key: 'cash', label: 'Наличные', Icon: Banknote, cls: 'emerald' }, { key: 'terminal', label: 'Терминал', Icon: CreditCard, cls: 'blue' }, ] as const).map(m => ( ))}
{/* Fiscal receipt (ATOL) */}

Фискальный чек (АТОЛ)

{atolConnected ? 'Регистратор подключён · автопечать чека' : 'Регистратор не подключён'}

{/* Notes */}
setNotes(e.target.value)} />
{/* Pay button */} ) : (

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

)}
{/* ── Right sidebar: Чеки (w-64) ── */}
{/* Shift totals */}
Чеки
{shiftReceipts.length}
Наличные {formatCurrency(cashRevenue)}
Терминал {formatCurrency(termRevenue)}
Итого {formatCurrency(shiftRevenue)}
{/* Receipt list — compact rows */}
{shiftReceipts.map(r => (
{format(r.createdAt, 'HH:mm')} №{r.roomNumber} {formatCurrency(r.amount)}
))}
)} {/* ══════════════════════ TAB: report ══════════════════════ */} {activeTab === 'report' && (
{/* X-report card */}
{/* Header */}

X-ОТЧЁТ (сменный)

Гранд Палас

Кассир: Елена Смирнова

{format(new Date(), 'd MMMM yyyy, HH:mm', { locale: ru })}

Смена №1 · открыта {format(shiftStartTime, 'HH:mm')} · {shiftTimer}
{/* Revenue table */}
Способ оплаты Чеков Сумма
Наличные {cashCount} {formatCurrency(cashRevenue)}
Терминал {termCount} {formatCurrency(termRevenue)}
ИТОГО {shiftReceipts.length} {formatCurrency(shiftRevenue)}
{/* Fiscal register info */}
Фискальный регистратор АТОЛ: {atolConnected ? подключён : не подключён}
{/* Refunds */}
Возвраты {formatCurrency(0)}
Чистая выручка {formatCurrency(shiftRevenue)}
{/* Footer note */}

Следующий шаг: закрыть смену (Z-отчёт) для передачи данных в ФНС

{/* Actions */}
)} {/* ══════════════════════ TAB: atol ══════════════════════ */} {activeTab === 'atol' && (
{/* Connection status */}

Подключение к АТОЛ

{atolConnected ? Подключено : Нет связи }
{/* Connection type */}

Тип подключения

{([ { key: 'usb', label: 'USB' }, { key: 'tcp', label: 'TCP/IP' }, { key: 'com', label: 'COM-порт' }, ] as const).map(ct => ( ))}
{/* TCP/IP fields */} {atolConnType === 'tcp' && (
setAtolIp(e.target.value)} placeholder="192.168.1.100" />
setAtolPort(e.target.value)} placeholder="9100" />
)} {/* COM port fields */} {atolConnType === 'com' && (
)}
{/* Cashier & tax */}

Параметры кассира

setAtolCashier(e.target.value)} placeholder="Фамилия Имя" />

Система налогообложения

{(Object.keys(TAX_SYSTEM_LABELS) as TaxSystem[]).map(ts => ( ))}
{/* Auto close toggle */}

Автоматически закрывать смену

В конце дня авто-печать Z-отчёта и закрытие смены

{/* Save button */}
)} )} {/* ── Receipt success overlay ── */} {lastReceipt && (

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

{formatCurrency(lastReceipt.amount)}

{lastReceipt.method === 'cash' ? 'Наличными' : 'Терминал'} · {format(lastReceipt.createdAt, 'HH:mm')} {printFiscal && atolConnected && · Фискальный чек распечатан}

Гость {lastReceipt.guestName}
Номер №{lastReceipt.roomNumber}
Назначение {lastReceipt.purposeLabel}
)} {/* ── Close shift confirmation modal ── */} {closeConfirm && (

Закрыть смену?

Будет напечатан Z-отчёт и смена закроется

Итого за смену {formatCurrency(shiftRevenue)}
Чеков {shiftReceipts.length}
)}
{/* end desktop-only wrapper */}
) }