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:
@@ -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'
|
||||||
@@ -82,9 +82,8 @@ const TAX_SYSTEM_LABELS: Record<TaxSystem, string> = {
|
|||||||
// ── Subcomponents ─────────────────────────────────────────────────────────────
|
// ── Subcomponents ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
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">
|
||||||
|
|||||||
Reference in New Issue
Block a user