664 lines
30 KiB
TypeScript
664 lines
30 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { useDemoStore, PosCartItem } from '../../mock/store';
|
||
import { INITIAL_CATALOG } from '../../mock/mockData';
|
||
import { Button, Badge, MaturityBadge, Modal, useToast } from '../../shared/ui';
|
||
import { formatCurrency } from '../../shared/utils/formatters';
|
||
import {
|
||
Search,
|
||
Plus,
|
||
Minus,
|
||
Trash2,
|
||
CreditCard,
|
||
Banknote,
|
||
QrCode,
|
||
Receipt,
|
||
CheckCircle2,
|
||
User,
|
||
Printer,
|
||
ChevronDown,
|
||
AlertCircle,
|
||
} from 'lucide-react';
|
||
|
||
const SUGGESTED_CUSTOMERS = [
|
||
{ name: 'Александр Романов', phone: '+7 (913) 900-11-22', tag: 'VIP • Скидка 10%' },
|
||
{ name: 'Елена Морозова', phone: '+7 (923) 456-78-90', tag: 'Постоянный гость' },
|
||
{ name: 'Михаил Васильев', phone: '+7 (905) 111-22-33', tag: 'Бонусная карта 5%' },
|
||
{ name: 'ООО Альфа Консалт', phone: '+7 (383) 200-40-50', tag: 'B2B безнал' },
|
||
];
|
||
|
||
export const PosView: React.FC = () => {
|
||
const { showToast } = useToast();
|
||
const {
|
||
posCart,
|
||
addToCart,
|
||
removeFromCart,
|
||
updateCartQuantity,
|
||
clearCart,
|
||
posDiscountPercent,
|
||
setPosDiscount,
|
||
posCustomerName,
|
||
setPosCustomer,
|
||
posOrderType,
|
||
setPosOrderType,
|
||
posTableNumber,
|
||
setPosTableNumber,
|
||
posPaymentModalOpen,
|
||
setPosPaymentModalOpen,
|
||
posLastReceipt,
|
||
completeCheckout,
|
||
getActiveProfile,
|
||
activeLocation,
|
||
} = useDemoStore();
|
||
|
||
const profile = getActiveProfile();
|
||
const [selectedCategory, setSelectedCategory] = useState<string>('Все');
|
||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||
const [showReceiptModal, setShowReceiptModal] = useState(false);
|
||
const [showCustomerPicker, setShowCustomerPicker] = useState(false);
|
||
|
||
// Split payment state
|
||
const [isSplitMode, setIsSplitMode] = useState(false);
|
||
const [splitCashAmount, setSplitCashAmount] = useState<number>(0);
|
||
const [splitCardAmount, setSplitCardAmount] = useState<number>(0);
|
||
|
||
const categories = ['Все', 'Роллы', 'Горячее', 'Супы', 'Автоуслуги', 'Автосервис'];
|
||
|
||
const filteredItems = INITIAL_CATALOG.filter((item) => {
|
||
const matchesCat = selectedCategory === 'Все' || item.category === selectedCategory;
|
||
const matchesSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||
return matchesCat && matchesSearch;
|
||
});
|
||
|
||
const subtotal = posCart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||
const discountAmount = (subtotal * posDiscountPercent) / 100;
|
||
const totalAmount = Math.max(0, subtotal - discountAmount);
|
||
|
||
const handlePayClick = () => {
|
||
if (posCart.length === 0) return;
|
||
setIsSplitMode(false);
|
||
setPosPaymentModalOpen(true);
|
||
};
|
||
|
||
const handleInitiateSplit = () => {
|
||
const half = Math.round(totalAmount / 2);
|
||
setSplitCashAmount(half);
|
||
setSplitCardAmount(totalAmount - half);
|
||
setIsSplitMode(true);
|
||
};
|
||
|
||
const handleConfirmSplitPayment = () => {
|
||
if (splitCashAmount + splitCardAmount !== totalAmount) return;
|
||
setPosPaymentModalOpen(false);
|
||
setIsSplitMode(false);
|
||
completeCheckout('split');
|
||
setShowReceiptModal(true);
|
||
};
|
||
|
||
const handlePaymentSelect = (method: 'cash' | 'card' | 'qr' | 'split') => {
|
||
if (method === 'split') {
|
||
handleInitiateSplit();
|
||
return;
|
||
}
|
||
setPosPaymentModalOpen(false);
|
||
completeCheckout(method);
|
||
setShowReceiptModal(true);
|
||
};
|
||
|
||
const handlePrintReceipt = () => {
|
||
showToast({
|
||
title: 'Печать фискального чека',
|
||
message: `Чек ${posLastReceipt?.orderNumber || ''} отправлен на кассовый регистратор (Атол 54-Ф)`,
|
||
type: 'success',
|
||
});
|
||
};
|
||
|
||
const remainingToSplit = totalAmount - (splitCashAmount + splitCardAmount);
|
||
|
||
return (
|
||
<div className="h-[calc(100vh-8.5rem)] flex flex-col md:flex-row gap-4 animate-fade-in overflow-hidden">
|
||
{/* Left: Product Catalog Grid */}
|
||
<div className="flex-1 flex flex-col min-w-0 bg-krona-surface border border-krona-border rounded-xl shadow-krona-soft overflow-hidden">
|
||
{/* Filter bar */}
|
||
<div className="p-3 border-b border-krona-border flex flex-wrap items-center justify-between gap-3 bg-krona-surface/70">
|
||
<div className="flex items-center gap-1.5 overflow-x-auto">
|
||
{categories.map((cat) => (
|
||
<button
|
||
key={cat}
|
||
onClick={() => setSelectedCategory(cat)}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||
selectedCategory === cat
|
||
? 'bg-[#2F684C] text-[#F8F6F0] shadow-sm'
|
||
: 'bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border'
|
||
}`}
|
||
>
|
||
{cat}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="relative w-56">
|
||
<Search className="w-3.5 h-3.5 absolute left-2.5 top-2.5 text-krona-secondary" />
|
||
<input
|
||
type="text"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
placeholder="Поиск по названию..."
|
||
className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-workspace border border-krona-border rounded-lg text-krona-primary placeholder:text-krona-secondary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Products Grid */}
|
||
<div className="flex-1 overflow-y-auto p-4 grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-3">
|
||
{filteredItems.map((item) => {
|
||
const inCart = posCart.find((c) => c.id === item.id);
|
||
const isStop = item.isStopListed;
|
||
|
||
return (
|
||
<div
|
||
key={item.id}
|
||
onClick={() => !isStop && addToCart(item)}
|
||
className={`group relative p-3.5 rounded-xl border flex flex-col justify-between transition-all select-none ${
|
||
isStop
|
||
? 'opacity-50 border-krona-danger/40 bg-krona-danger/5 cursor-not-allowed'
|
||
: 'cursor-pointer border-krona-border bg-krona-surface hover:border-krona-green/60 hover:shadow-krona-soft active:scale-[0.99]'
|
||
} ${inCart ? 'ring-2 ring-krona-green/40' : ''}`}
|
||
>
|
||
<div>
|
||
<div className="flex items-center justify-between gap-1 mb-1.5">
|
||
<span className="text-[10px] text-krona-secondary uppercase font-semibold">
|
||
{item.category}
|
||
</span>
|
||
{isStop ? (
|
||
<Badge variant="danger" size="sm">Стоп-лист</Badge>
|
||
) : inCart ? (
|
||
<Badge variant="green" size="sm">{inCart.quantity} в чеке</Badge>
|
||
) : null}
|
||
</div>
|
||
<h4 className="text-xs font-bold text-krona-primary leading-snug line-clamp-2">
|
||
{item.name}
|
||
</h4>
|
||
{item.description && (
|
||
<p className="text-[11px] text-krona-secondary line-clamp-2 mt-1">
|
||
{item.description}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-3 pt-2.5 border-t border-krona-border/40 flex items-baseline justify-between">
|
||
<span className="text-sm font-bold text-krona-primary">
|
||
{formatCurrency(item.price)}
|
||
</span>
|
||
<span className="text-[10px] text-krona-secondary">
|
||
Себест: {item.cost} ₽ ({item.marginPercent}%)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Cart & Order Register Panel */}
|
||
<div className="w-full md:w-80 lg:w-96 flex flex-col bg-krona-surface border border-krona-border rounded-xl shadow-krona-soft overflow-hidden">
|
||
{/* Order settings header */}
|
||
<div className="p-3 border-b border-krona-border bg-krona-surface/80 space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs font-bold text-krona-primary uppercase tracking-wider">
|
||
Текущий чек
|
||
</span>
|
||
<MaturityBadge level="target" size="sm" />
|
||
</div>
|
||
<span className="text-xs text-krona-secondary">{activeLocation}</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-1 bg-krona-workspace p-1 rounded-lg border border-krona-border text-xs">
|
||
<button
|
||
onClick={() => setPosOrderType('hall')}
|
||
className={`py-1 rounded font-medium transition-colors ${
|
||
posOrderType === 'hall' ? 'bg-[#2F684C] text-white shadow-sm' : 'text-krona-secondary'
|
||
}`}
|
||
>
|
||
В зале
|
||
</button>
|
||
<button
|
||
onClick={() => setPosOrderType('takeaway')}
|
||
className={`py-1 rounded font-medium transition-colors ${
|
||
posOrderType === 'takeaway' ? 'bg-[#2F684C] text-white shadow-sm' : 'text-krona-secondary'
|
||
}`}
|
||
>
|
||
Навынос
|
||
</button>
|
||
<button
|
||
onClick={() => setPosOrderType('delivery')}
|
||
className={`py-1 rounded font-medium transition-colors ${
|
||
posOrderType === 'delivery' ? 'bg-[#2F684C] text-white shadow-sm' : 'text-krona-secondary'
|
||
}`}
|
||
>
|
||
Доставка
|
||
</button>
|
||
</div>
|
||
|
||
{/* Customer picker */}
|
||
<div className="relative">
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex-1 flex items-center gap-1.5 px-2.5 py-1 bg-krona-workspace border border-krona-border rounded-md text-xs">
|
||
<User className="w-3.5 h-3.5 text-krona-secondary" />
|
||
<input
|
||
type="text"
|
||
value={posCustomerName}
|
||
onChange={(e) => setPosCustomer(e.target.value)}
|
||
placeholder="Клиент / Выбрать..."
|
||
className="w-full bg-transparent focus:outline-none text-xs text-krona-primary"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowCustomerPicker(!showCustomerPicker)}
|
||
className="text-krona-secondary hover:text-krona-primary"
|
||
title="Выбрать из справочника"
|
||
>
|
||
<ChevronDown className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
|
||
{posOrderType === 'hall' && (
|
||
<div className="w-24">
|
||
<input
|
||
type="text"
|
||
value={posTableNumber}
|
||
onChange={(e) => setPosTableNumber(e.target.value)}
|
||
placeholder="Стол..."
|
||
className="w-full px-2 py-1 bg-krona-workspace border border-krona-border rounded-md text-xs text-krona-primary text-center font-medium focus:outline-none"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Quick Customer Picker Dropdown */}
|
||
{showCustomerPicker && (
|
||
<div className="absolute left-0 right-0 top-full mt-1 bg-krona-surface border border-krona-border rounded-xl shadow-lg z-20 p-2 space-y-1 text-xs">
|
||
<div className="text-[10px] font-bold text-krona-secondary px-2 py-1 uppercase">
|
||
Быстрый выбор клиента CRM
|
||
</div>
|
||
{SUGGESTED_CUSTOMERS.map((cust, idx) => (
|
||
<button
|
||
key={idx}
|
||
type="button"
|
||
onClick={() => {
|
||
setPosCustomer(cust.name);
|
||
setShowCustomerPicker(false);
|
||
if (cust.tag.includes('10%')) setPosDiscount(10);
|
||
else if (cust.tag.includes('5%')) setPosDiscount(5);
|
||
}}
|
||
className="w-full text-left p-2 rounded-lg hover:bg-krona-workspace flex items-center justify-between text-xs transition-colors"
|
||
>
|
||
<div>
|
||
<div className="font-semibold text-krona-primary">{cust.name}</div>
|
||
<div className="text-[10px] text-krona-secondary">{cust.phone}</div>
|
||
</div>
|
||
<Badge variant="gold" size="sm">{cust.tag}</Badge>
|
||
</button>
|
||
))}
|
||
<div className="pt-1 border-t border-krona-border/50 text-center">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowCustomerPicker(false)}
|
||
className="text-[11px] text-krona-secondary hover:text-krona-primary"
|
||
>
|
||
Закрыть список
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Cart items list */}
|
||
<div className="flex-1 overflow-y-auto p-3 divide-y divide-krona-border/40">
|
||
{posCart.length === 0 ? (
|
||
<div className="py-16 text-center text-xs text-krona-secondary">
|
||
<Receipt className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||
Чек пуст. Добавьте позиции из каталога слева.
|
||
</div>
|
||
) : (
|
||
posCart.map((item) => (
|
||
<div key={item.id} className="py-2.5 first:pt-0 last:pb-0 flex items-center justify-between gap-2 text-xs">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="font-semibold text-krona-primary truncate">{item.name}</div>
|
||
<div className="text-[11px] text-krona-secondary">
|
||
{formatCurrency(item.price)} × {item.quantity} = {formatCurrency(item.price * item.quantity)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1 shrink-0">
|
||
<button
|
||
onClick={() => updateCartQuantity(item.id, -1)}
|
||
className="w-6 h-6 rounded bg-krona-workspace border border-krona-border flex items-center justify-center hover:bg-krona-border/50 text-krona-primary"
|
||
aria-label={`Уменьшить количество ${item.name}`}
|
||
>
|
||
<Minus className="w-3 h-3" />
|
||
</button>
|
||
<span className="w-6 text-center font-semibold text-xs text-krona-primary">
|
||
{item.quantity}
|
||
</span>
|
||
<button
|
||
onClick={() => updateCartQuantity(item.id, 1)}
|
||
className="w-6 h-6 rounded bg-krona-workspace border border-krona-border flex items-center justify-center hover:bg-krona-border/50 text-krona-primary"
|
||
aria-label={`Увеличить количество ${item.name}`}
|
||
>
|
||
<Plus className="w-3 h-3" />
|
||
</button>
|
||
<button
|
||
onClick={() => removeFromCart(item.id)}
|
||
className="p-1 text-krona-secondary hover:text-krona-danger ml-1"
|
||
aria-label={`Удалить ${item.name} из чека`}
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* Discount & Totals */}
|
||
<div className="p-3.5 border-t border-krona-border bg-krona-surface/80 space-y-2 text-xs">
|
||
{/* Quick Discounts */}
|
||
<div className="flex items-center justify-between gap-1 text-[11px]">
|
||
<span className="text-krona-secondary">Скидка:</span>
|
||
<div className="flex items-center gap-1">
|
||
{[0, 5, 10, 15, 20].map((pct) => (
|
||
<button
|
||
key={pct}
|
||
onClick={() => setPosDiscount(pct)}
|
||
className={`px-2 py-0.5 rounded border transition-colors ${
|
||
posDiscountPercent === pct
|
||
? 'bg-krona-gold text-white font-bold border-krona-gold'
|
||
: 'bg-krona-workspace border-krona-border text-krona-secondary'
|
||
}`}
|
||
>
|
||
{pct}%
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1 pt-1">
|
||
<div className="flex justify-between text-krona-secondary">
|
||
<span>Сумма позиций:</span>
|
||
<span>{formatCurrency(subtotal)}</span>
|
||
</div>
|
||
{posDiscountPercent > 0 && (
|
||
<div className="flex justify-between text-krona-danger">
|
||
<span>Скидка ({posDiscountPercent}%):</span>
|
||
<span>-{formatCurrency(discountAmount)}</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between text-base font-bold text-krona-primary pt-1 border-t border-krona-border">
|
||
<span>К оплате:</span>
|
||
<span className="text-krona-green">{formatCurrency(totalAmount)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-2 flex gap-2">
|
||
<Button
|
||
variant="secondary"
|
||
size="md"
|
||
onClick={clearCart}
|
||
disabled={posCart.length === 0}
|
||
className="px-3"
|
||
title="Очистить чек"
|
||
>
|
||
Сброс
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
size="md"
|
||
onClick={handlePayClick}
|
||
disabled={posCart.length === 0}
|
||
className="flex-1 text-sm font-semibold"
|
||
>
|
||
Оплатить {formatCurrency(totalAmount)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modal: Payment Method Selection & Split Payment */}
|
||
<Modal
|
||
isOpen={posPaymentModalOpen}
|
||
onClose={() => {
|
||
setPosPaymentModalOpen(false);
|
||
setIsSplitMode(false);
|
||
}}
|
||
title={isSplitMode ? "Смешанная оплата (Наличные + Карта)" : "Оплата заказа"}
|
||
subtitle={`Сумма к оплате: ${formatCurrency(totalAmount)}`}
|
||
maxWidth="md"
|
||
>
|
||
{!isSplitMode ? (
|
||
<div className="space-y-3">
|
||
<p className="text-xs text-krona-secondary">
|
||
Выберите способ расчета клиента на кассе точки «{activeLocation}»:
|
||
</p>
|
||
|
||
<div className="grid grid-cols-2 gap-3 pt-1">
|
||
<button
|
||
onClick={() => handlePaymentSelect('card')}
|
||
className="p-4 rounded-xl border border-krona-border hover:border-krona-green hover:bg-krona-green/5 text-left flex flex-col items-center justify-center gap-2 group transition-all"
|
||
>
|
||
<CreditCard className="w-8 h-8 text-krona-green group-hover:scale-110 transition-transform" />
|
||
<div className="text-xs font-bold text-krona-primary">Банковская карта</div>
|
||
<div className="text-[10px] text-krona-secondary">Терминал эквайринга</div>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => handlePaymentSelect('cash')}
|
||
className="p-4 rounded-xl border border-krona-border hover:border-krona-green hover:bg-krona-green/5 text-left flex flex-col items-center justify-center gap-2 group transition-all"
|
||
>
|
||
<Banknote className="w-8 h-8 text-krona-green group-hover:scale-110 transition-transform" />
|
||
<div className="text-xs font-bold text-krona-primary">Наличные</div>
|
||
<div className="text-[10px] text-krona-secondary">С выдачей сдачи</div>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => handlePaymentSelect('qr')}
|
||
className="p-4 rounded-xl border border-krona-border hover:border-krona-green hover:bg-krona-green/5 text-left flex flex-col items-center justify-center gap-2 group transition-all"
|
||
>
|
||
<QrCode className="w-8 h-8 text-krona-gold group-hover:scale-110 transition-transform" />
|
||
<div className="text-xs font-bold text-krona-primary">СБП / QR-код</div>
|
||
<div className="text-[10px] text-krona-secondary">Мгновенное зачисление</div>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => handlePaymentSelect('split')}
|
||
className="p-4 rounded-xl border border-krona-border hover:border-krona-green hover:bg-krona-green/5 text-left flex flex-col items-center justify-center gap-2 group transition-all"
|
||
>
|
||
<Receipt className="w-8 h-8 text-krona-secondary group-hover:scale-110 transition-transform" />
|
||
<div className="text-xs font-bold text-krona-primary">Смешанная оплата</div>
|
||
<div className="text-[10px] text-krona-secondary">Часть нал + часть карта</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4 text-xs">
|
||
<div className="bg-krona-workspace p-3 rounded-lg border border-krona-border space-y-2">
|
||
<div className="flex justify-between font-bold text-krona-primary">
|
||
<span>Общая сумма к оплате:</span>
|
||
<span>{formatCurrency(totalAmount)}</span>
|
||
</div>
|
||
<p className="text-[11px] text-krona-secondary">
|
||
Распределите сумму между наличным расчетом и банковской картой:
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="space-y-1">
|
||
<label className="text-[11px] font-semibold text-krona-secondary flex items-center gap-1">
|
||
<Banknote className="w-3.5 h-3.5 text-krona-green" /> Сумма наличными (₽):
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min={0}
|
||
max={totalAmount}
|
||
value={splitCashAmount}
|
||
onChange={(e) => {
|
||
const val = Math.max(0, Number(e.target.value));
|
||
setSplitCashAmount(val);
|
||
setSplitCardAmount(Math.max(0, totalAmount - val));
|
||
}}
|
||
className="w-full p-2 bg-krona-surface border border-krona-border rounded-lg font-mono font-bold text-sm text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-[11px] font-semibold text-krona-secondary flex items-center gap-1">
|
||
<CreditCard className="w-3.5 h-3.5 text-sky-600" /> Сумма картой (₽):
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min={0}
|
||
max={totalAmount}
|
||
value={splitCardAmount}
|
||
onChange={(e) => {
|
||
const val = Math.max(0, Number(e.target.value));
|
||
setSplitCardAmount(val);
|
||
setSplitCashAmount(Math.max(0, totalAmount - val));
|
||
}}
|
||
className="w-full p-2 bg-krona-surface border border-krona-border rounded-lg font-mono font-bold text-sm text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Balance check indicator */}
|
||
<div className={`p-2.5 rounded-lg border flex items-center justify-between ${
|
||
remainingToSplit === 0
|
||
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-700 dark:text-emerald-300'
|
||
: 'bg-rose-500/10 border-rose-500/30 text-rose-700 dark:text-rose-300'
|
||
}`}>
|
||
<div className="flex items-center gap-1.5 font-medium">
|
||
{remainingToSplit === 0 ? (
|
||
<>
|
||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||
<span>Сумма распределена верно (100%)</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<AlertCircle className="w-4 h-4 text-rose-600" />
|
||
<span>{remainingToSplit > 0 ? `Не распределено: ${formatCurrency(remainingToSplit)}` : `Переплата: ${formatCurrency(Math.abs(remainingToSplit))}`}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
<span className="font-mono font-bold">{formatCurrency(splitCashAmount + splitCardAmount)} / {formatCurrency(totalAmount)}</span>
|
||
</div>
|
||
|
||
<div className="flex gap-2 pt-2 border-t border-krona-border">
|
||
<Button
|
||
variant="secondary"
|
||
size="md"
|
||
onClick={() => setIsSplitMode(false)}
|
||
className="w-1/3"
|
||
>
|
||
Назад
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
size="md"
|
||
onClick={handleConfirmSplitPayment}
|
||
disabled={remainingToSplit !== 0}
|
||
className="flex-1"
|
||
>
|
||
Подтвердить раздельную оплату
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
|
||
{/* Modal: Fiscal Receipt Details */}
|
||
{showReceiptModal && posLastReceipt && (
|
||
<Modal
|
||
isOpen={showReceiptModal}
|
||
onClose={() => setShowReceiptModal(false)}
|
||
title="Чек успешно пробит!"
|
||
subtitle={`Заказ ${posLastReceipt.orderNumber} • ${posLastReceipt.timestamp}`}
|
||
maxWidth="sm"
|
||
footer={
|
||
<div className="flex gap-2 w-full">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={handlePrintReceipt}
|
||
className="flex-1 flex items-center justify-center gap-1.5"
|
||
>
|
||
<Printer className="w-4 h-4" />
|
||
Распечатать чек
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
onClick={() => setShowReceiptModal(false)}
|
||
className="flex-1"
|
||
>
|
||
Новый заказ
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="font-mono text-xs bg-krona-workspace p-3.5 rounded-lg border border-krona-border space-y-2">
|
||
<div className="text-center font-bold pb-1 border-b border-dashed border-krona-border">
|
||
{profile.companyName}
|
||
<div className="text-[10px] font-normal text-krona-secondary">{posLastReceipt.location}</div>
|
||
<div className="text-[10px] text-krona-secondary font-normal">Касса №1 • Смена открыта</div>
|
||
</div>
|
||
|
||
<div className="space-y-1 py-1">
|
||
{posLastReceipt.items.map((it: PosCartItem, i: number) => (
|
||
<div key={i} className="flex justify-between">
|
||
<span>{it.name} × {it.quantity}</span>
|
||
<span>{formatCurrency(it.price * it.quantity)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="pt-2 border-t border-dashed border-krona-border space-y-1">
|
||
<div className="flex justify-between">
|
||
<span>Сумма:</span>
|
||
<span>{formatCurrency(posLastReceipt.subtotal)}</span>
|
||
</div>
|
||
{posLastReceipt.discount > 0 && (
|
||
<div className="flex justify-between text-krona-danger">
|
||
<span>Скидка:</span>
|
||
<span>-{formatCurrency(posLastReceipt.discount)}</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between font-bold text-sm pt-1 border-t border-dotted border-krona-border">
|
||
<span>ИТОГ К ОПЛАТЕ:</span>
|
||
<span>{formatCurrency(posLastReceipt.total)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-[11px] text-krona-secondary">
|
||
<span>Способ оплаты:</span>
|
||
<span className="uppercase font-semibold">{posLastReceipt.paymentType}</span>
|
||
</div>
|
||
{posLastReceipt.customer && (
|
||
<div className="flex justify-between text-[11px] text-krona-secondary">
|
||
<span>Клиент:</span>
|
||
<span>{posLastReceipt.customer}</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between text-[10px] text-krona-secondary pt-1">
|
||
<span>ФП (фискальный признак):</span>
|
||
<span>ФП: 49204910283</span>
|
||
</div>
|
||
<div className="flex justify-between text-[10px] text-krona-secondary">
|
||
<span>ФН: 996044030198 • РН ККТ: 00049281001</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-2 text-center text-[10px] text-krona-secondary border-t border-dashed border-krona-border">
|
||
Спасибо за заказ в «{profile.companyName}»!
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|