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 && (
)}
)}
)
}
// ── 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 (
)
}
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 && (
)}
{/* Диалог наличных */}
{cashDialog && (
setCashDialog(null)}
/>
)}
)
}