Fix POS: АТОЛ is a fiscal register, not a payment method

- Remove АТОЛ from payment method selector (now only Наличные / Терминал)
- Add fiscal receipt block below payment: shows АТОЛ connection status + toggle to print fiscal receipt
- Remove atolRevenue/atolCount from shift totals and X-report
- Add АТОЛ connection status line to X-report
- Success overlay shows 'Фискальный чек распечатан' when АТОЛ connected and print enabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 19:22:05 +03:00
parent 6dabb29b61
commit 5e3fca5471

View File

@@ -22,7 +22,7 @@ interface RoomBalance {
nights: number nights: number
} }
type PaymentMethod = 'cash' | 'terminal' | 'atol' type PaymentMethod = 'cash' | 'terminal'
type ChargePurpose = 'stay' | 'breakfast' | 'transfer' | 'parking' | 'extra' type ChargePurpose = 'stay' | 'breakfast' | 'transfer' | 'parking' | 'extra'
type PosTab = 'main' | 'report' | 'atol' type PosTab = 'main' | 'report' | 'atol'
type AtolConnection = 'usb' | 'tcp' | 'com' type AtolConnection = 'usb' | 'tcp' | 'com'
@@ -83,8 +83,7 @@ const TAX_SYSTEM_LABELS: Record<TaxSystem, string> = {
function MethodIcon({ method, size = 11 }: { method: PaymentMethod; size?: number }) { function MethodIcon({ method, size = 11 }: { method: PaymentMethod; size?: number }) {
if (method === 'cash') return <Banknote size={size} className="text-emerald-600" /> if (method === 'cash') return <Banknote size={size} className="text-emerald-600" />
if (method === 'terminal') return <CreditCard size={size} className="text-blue-600" /> return <CreditCard size={size} className="text-blue-600" />
return <Printer size={size} className="text-violet-600" />
} }
// ── Component ───────────────────────────────────────────────────────────────── // ── Component ─────────────────────────────────────────────────────────────────
@@ -100,6 +99,7 @@ export function PosPage() {
const [amount, setAmount] = useState<number>(0) const [amount, setAmount] = useState<number>(0)
const [method, setMethod] = useState<PaymentMethod>('terminal') const [method, setMethod] = useState<PaymentMethod>('terminal')
const [notes, setNotes] = useState('') const [notes, setNotes] = useState('')
const [printFiscal, setPrintFiscal] = useState(true)
const [lastReceipt, setLastReceipt] = useState<ReceiptRecord | null>(null) const [lastReceipt, setLastReceipt] = useState<ReceiptRecord | null>(null)
const [activeTab, setActiveTab] = useState<PosTab>('main') const [activeTab, setActiveTab] = useState<PosTab>('main')
@@ -151,10 +151,8 @@ export function PosPage() {
const shiftRevenue = shiftReceipts.reduce((s, r) => s + r.amount, 0) 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 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 termRevenue = shiftReceipts.filter(r => r.method === 'terminal').reduce((s, r) => s + r.amount, 0)
const atolRevenue = shiftReceipts.filter(r => r.method === 'atol').reduce((s, r) => s + r.amount, 0)
const cashCount = shiftReceipts.filter(r => r.method === 'cash').length const cashCount = shiftReceipts.filter(r => r.method === 'cash').length
const termCount = shiftReceipts.filter(r => r.method === 'terminal').length const termCount = shiftReceipts.filter(r => r.method === 'terminal').length
const atolCount = shiftReceipts.filter(r => r.method === 'atol').length
const handlePay = () => { const handlePay = () => {
if (!selectedRoom || amount <= 0 || !shiftOpen) return if (!selectedRoom || amount <= 0 || !shiftOpen) return
@@ -390,11 +388,10 @@ export function PosPage() {
{/* Payment method */} {/* Payment method */}
<div> <div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Способ оплаты</p> <p className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Способ оплаты</p>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-2 gap-2">
{([ {([
{ key: 'cash', label: 'Наличные', Icon: Banknote, cls: 'emerald' }, { key: 'cash', label: 'Наличные', Icon: Banknote, cls: 'emerald' },
{ key: 'terminal', label: 'Терминал', Icon: CreditCard, cls: 'blue' }, { key: 'terminal', label: 'Терминал', Icon: CreditCard, cls: 'blue' },
{ key: 'atol', label: 'АТОЛ', Icon: Printer, cls: 'violet' },
] as const).map(m => ( ] as const).map(m => (
<button <button
key={m.key} key={m.key}
@@ -403,19 +400,46 @@ export function PosPage() {
'flex flex-col items-center gap-2 py-3 rounded-xl border-2 text-sm font-medium transition-colors', 'flex flex-col items-center gap-2 py-3 rounded-xl border-2 text-sm font-medium transition-colors',
method === m.key method === m.key
? m.cls === 'emerald' ? 'bg-emerald-600 border-emerald-600 text-white' ? m.cls === 'emerald' ? 'bg-emerald-600 border-emerald-600 text-white'
: m.cls === 'blue' ? 'bg-blue-600 border-blue-600 text-white' : 'bg-blue-600 border-blue-600 text-white'
: 'bg-violet-600 border-violet-600 text-white'
: 'border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:border-slate-300', : 'border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:border-slate-300',
)} )}
> >
<m.Icon size={18} /> <m.Icon size={18} />
{m.label} {m.label}
{m.key === 'atol' && <span className="text-[10px] opacity-70">интеграция</span>}
</button> </button>
))} ))}
</div> </div>
</div> </div>
{/* Fiscal receipt (ATOL) */}
<div className={cn(
'flex items-center justify-between gap-3 px-4 py-3 rounded-xl border-2 transition-colors',
atolConnected
? printFiscal
? 'border-violet-400 bg-violet-50 dark:bg-violet-900/20'
: 'border-slate-200 dark:border-slate-600'
: 'border-slate-200 dark:border-slate-600 opacity-50',
)}>
<div className="flex items-center gap-3">
<Printer size={18} className={atolConnected && printFiscal ? 'text-violet-600' : 'text-slate-400'} />
<div>
<p className="text-sm font-medium text-slate-800 dark:text-slate-200">
Фискальный чек (АТОЛ)
</p>
<p className="text-[11px] text-slate-500 dark:text-slate-400">
{atolConnected ? 'Регистратор подключён · автопечать чека' : 'Регистратор не подключён'}
</p>
</div>
</div>
<button
onClick={() => atolConnected && setPrintFiscal(v => !v)}
disabled={!atolConnected}
className={cn('relative w-11 h-6 rounded-full transition-colors shrink-0', printFiscal && atolConnected ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-600')}
>
<div className={cn('absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform', printFiscal && atolConnected ? 'left-[22px]' : 'left-1')} />
</button>
</div>
{/* Notes */} {/* Notes */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Примечание</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Примечание</label>
@@ -467,10 +491,6 @@ export function PosPage() {
<span className="flex items-center gap-1"><CreditCard size={9} className="text-blue-600" />Терминал</span> <span className="flex items-center gap-1"><CreditCard size={9} className="text-blue-600" />Терминал</span>
<span className="font-semibold text-slate-700 dark:text-slate-300">{formatCurrency(termRevenue)}</span> <span className="font-semibold text-slate-700 dark:text-slate-300">{formatCurrency(termRevenue)}</span>
</div> </div>
<div className="flex justify-between text-[10px] text-slate-500">
<span className="flex items-center gap-1"><Printer size={9} className="text-violet-600" />АТОЛ</span>
<span className="font-semibold text-slate-700 dark:text-slate-300">{formatCurrency(atolRevenue)}</span>
</div>
<div className="flex justify-between text-[10px] border-t border-slate-200 dark:border-slate-600 pt-0.5 mt-0.5"> <div className="flex justify-between text-[10px] border-t border-slate-200 dark:border-slate-600 pt-0.5 mt-0.5">
<span className="font-semibold text-slate-600 dark:text-slate-400">Итого</span> <span className="font-semibold text-slate-600 dark:text-slate-400">Итого</span>
<span className="font-bold text-slate-900 dark:text-slate-100">{formatCurrency(shiftRevenue)}</span> <span className="font-bold text-slate-900 dark:text-slate-100">{formatCurrency(shiftRevenue)}</span>
@@ -541,14 +561,6 @@ export function PosPage() {
<td className="py-2.5 text-right text-slate-500 text-xs">{termCount}</td> <td className="py-2.5 text-right text-slate-500 text-xs">{termCount}</td>
<td className="py-2.5 text-right font-semibold text-slate-900 dark:text-slate-100">{formatCurrency(termRevenue)}</td> <td className="py-2.5 text-right font-semibold text-slate-900 dark:text-slate-100">{formatCurrency(termRevenue)}</td>
</tr> </tr>
<tr>
<td className="py-2.5 flex items-center gap-2">
<Printer size={14} className="text-violet-600" />
<span className="text-slate-700 dark:text-slate-300">АТОЛ</span>
</td>
<td className="py-2.5 text-right text-slate-500 text-xs">{atolCount}</td>
<td className="py-2.5 text-right font-semibold text-slate-900 dark:text-slate-100">{formatCurrency(atolRevenue)}</td>
</tr>
<tr className="border-t-2 border-slate-300 dark:border-slate-500"> <tr className="border-t-2 border-slate-300 dark:border-slate-500">
<td className="pt-3 pb-2 font-bold text-slate-900 dark:text-slate-100">ИТОГО</td> <td className="pt-3 pb-2 font-bold text-slate-900 dark:text-slate-100">ИТОГО</td>
<td className="pt-3 pb-2 text-right font-bold text-slate-500">{shiftReceipts.length}</td> <td className="pt-3 pb-2 text-right font-bold text-slate-500">{shiftReceipts.length}</td>
@@ -557,6 +569,12 @@ export function PosPage() {
</tbody> </tbody>
</table> </table>
{/* Fiscal register info */}
<div className="mt-3 flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
<Printer size={13} className="text-violet-500" />
Фискальный регистратор АТОЛ: {atolConnected ? <span className="text-emerald-600 font-medium">подключён</span> : <span className="text-red-500 font-medium">не подключён</span>}
</div>
{/* Refunds */} {/* Refunds */}
<div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-600"> <div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-600">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
@@ -769,7 +787,8 @@ export function PosPage() {
<p className="text-xl font-bold text-slate-900 dark:text-slate-100">Оплата принята</p> <p className="text-xl font-bold text-slate-900 dark:text-slate-100">Оплата принята</p>
<p className="text-3xl font-bold text-emerald-600 mt-1">{formatCurrency(lastReceipt.amount)}</p> <p className="text-3xl font-bold text-emerald-600 mt-1">{formatCurrency(lastReceipt.amount)}</p>
<p className="text-sm text-slate-500 mt-1"> <p className="text-sm text-slate-500 mt-1">
{lastReceipt.method === 'cash' ? 'Наличными' : lastReceipt.method === 'terminal' ? 'Терминал' : 'АТОЛ'} · {format(lastReceipt.createdAt, 'HH:mm')} {lastReceipt.method === 'cash' ? 'Наличными' : 'Терминал'} · {format(lastReceipt.createdAt, 'HH:mm')}
{printFiscal && atolConnected && <span className="ml-2 text-violet-600 font-medium">· Фискальный чек распечатан</span>}
</p> </p>
</div> </div>
<div className="text-left bg-slate-50 dark:bg-slate-700/50 rounded-xl p-3 space-y-1"> <div className="text-left bg-slate-50 dark:bg-slate-700/50 rounded-xl p-3 space-y-1">