import { useMutation, useQueryClient } from '@tanstack/react-query' import { usePOSStore } from '@/stores/pos.store' import { posMutations, posKeys, type Transaction } from '@/api/pos' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { X, Banknote, CreditCard, FileText, Ban, UserRound, Tag } from 'lucide-react' import { toast } from 'sonner' import { useState } from 'react' import { POSPaymentDialog } from './pos-payment-dialog' import { POSCustomerDialog } from './pos-customer-dialog' import { ManagerOverrideDialog, requiresOverride, requiresDiscountOverride } from './pos-manager-override' import type { TransactionLineItem } from '@/api/pos' interface POSCartPanelProps { transaction: Transaction | null } export function POSCartPanel({ transaction }: POSCartPanelProps) { const queryClient = useQueryClient() const { currentTransactionId, setTransaction, accountName, accountPhone, accountEmail } = usePOSStore() const [paymentMethod, setPaymentMethod] = useState(null) const [customerOpen, setCustomerOpen] = useState(false) const [overrideOpen, setOverrideOpen] = useState(false) const [priceItemId, setPriceItemId] = useState(null) const [pendingDiscount, setPendingDiscount] = useState<{ lineItemId: string; amount: number; reason: string } | null>(null) const [pendingOrderDiscount, setPendingOrderDiscount] = useState<{ amount: number; reason: string } | null>(null) const [discountOverrideOpen, setDiscountOverrideOpen] = useState(false) const lineItems = transaction?.lineItems ?? [] const drawerSessionId = usePOSStore((s) => s.drawerSessionId) const drawerOpen = !!drawerSessionId const removeItemMutation = useMutation({ mutationFn: (lineItemId: string) => posMutations.removeLineItem(currentTransactionId!, lineItemId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) }) }, onError: (err) => toast.error(err.message), }) const discountMutation = useMutation({ mutationFn: (data: { lineItemId: string; amount: number; reason: string }) => posMutations.applyDiscount(currentTransactionId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) }) setPriceItemId(null) toast.success('Price adjusted') }, onError: (err) => toast.error(err.message), }) const orderDiscountMutation = useMutation({ mutationFn: async ({ amount, reason }: { amount: number; reason: string }) => { // Distribute discount proportionally across all line items let remaining = amount for (let i = 0; i < lineItems.length; i++) { const item = lineItems[i] const itemTotal = parseFloat(item.unitPrice) * item.qty const isLast = i === lineItems.length - 1 const share = isLast ? remaining : Math.round((itemTotal / subtotal) * amount * 100) / 100 remaining -= share if (share > 0) { await posMutations.applyDiscount(currentTransactionId!, { lineItemId: item.id, amount: share, reason }) } } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) }) toast.success('Order discount applied') }, onError: (err) => toast.error(err.message), }) const voidMutation = useMutation({ mutationFn: () => posMutations.void(currentTransactionId!), onSuccess: () => { setTransaction(null) toast.success('Transaction voided') }, onError: (err) => toast.error(err.message), }) const subtotal = parseFloat(transaction?.subtotal ?? '0') const discountTotal = parseFloat(transaction?.discountTotal ?? '0') const taxTotal = parseFloat(transaction?.taxTotal ?? '0') const total = parseFloat(transaction?.total ?? '0') const hasItems = lineItems.length > 0 const isPending = transaction?.status === 'pending' function handlePaymentComplete() { setPaymentMethod(null) setTransaction(null) } return (
{/* Header */}

Current Sale

{transaction && ( {transaction.transactionNumber} )}
{/* Line items */}
{lineItems.length === 0 ? (
No items yet
) : (
{lineItems.map((item) => { const unitPrice = parseFloat(item.unitPrice) const discount = parseFloat(item.discountAmount) const hasDiscount = discount > 0 const listTotal = unitPrice * item.qty const discountPct = listTotal > 0 ? Math.round((discount / listTotal) * 100) : 0 return (

{item.description}

{item.qty} x ${unitPrice.toFixed(2)} {hasDiscount && ( -${discount.toFixed(2)} ({discountPct}%) )} {parseFloat(item.taxAmount) > 0 && ( tax ${parseFloat(item.taxAmount).toFixed(2)} )}
${parseFloat(item.lineTotal).toFixed(2)} {isPending && (
setPriceItemId(o ? item.id : null)} onApply={(amount, reason) => { const pct = listTotal > 0 ? (amount / listTotal) * 100 : 0 if (requiresDiscountOverride(pct)) { setPendingDiscount({ lineItemId: item.id, amount, reason }) setDiscountOverrideOpen(true) } else { discountMutation.mutate({ lineItemId: item.id, amount, reason }) } }} isPending={discountMutation.isPending} />
)}
) })}
)}
{/* Totals + payment */}
Subtotal ${subtotal.toFixed(2)}
{discountTotal > 0 && (
Discount -${discountTotal.toFixed(2)}
)}
Tax ${taxTotal.toFixed(2)}
Total ${total.toFixed(2)}
{/* Order discount button */} {hasItems && isPending && (
{ const pct = subtotal > 0 ? (amount / subtotal) * 100 : 0 if (requiresDiscountOverride(pct)) { setPendingOrderDiscount({ amount, reason }) setDiscountOverrideOpen(true) } else { orderDiscountMutation.mutate({ amount, reason }) } }} isPending={orderDiscountMutation.isPending} />
)} {/* Payment buttons */}
{!drawerOpen && hasItems && (

Open the drawer before accepting payment

)}
{/* Payment dialog */} {paymentMethod && transaction && ( { if (!open) setPaymentMethod(null) }} paymentMethod={paymentMethod} transaction={transaction} onComplete={handlePaymentComplete} /> )} {/* Customer dialog */} {/* Manager override for void */} voidMutation.mutate()} /> {/* Manager override for discount */} { if (pendingDiscount) { discountMutation.mutate(pendingDiscount) setPendingDiscount(null) } else if (pendingOrderDiscount) { orderDiscountMutation.mutate(pendingOrderDiscount) setPendingOrderDiscount(null) } }} />
) } // --- Order Discount Button --- function OrderDiscountButton({ subtotal, onApply, isPending }: { subtotal: number onApply: (amount: number, reason: string) => void isPending: boolean }) { const [open, setOpen] = useState(false) const [mode, setMode] = useState('percent') const [value, setValue] = useState('') function calculate() { const v = parseFloat(value) || 0 if (mode === 'amount_off') return Math.min(v, subtotal) if (mode === 'set_price') return Math.max(0, subtotal - v) return Math.round(subtotal * (v / 100) * 100) / 100 } const discountAmount = calculate() function handleApply() { if (discountAmount <= 0) return const reason = mode === 'percent' ? `${parseFloat(value)}% order discount` : mode === 'set_price' ? `Order total set to $${parseFloat(value).toFixed(2)}` : `$${parseFloat(value).toFixed(2)} order discount` onApply(discountAmount, reason) setValue('') setOpen(false) } return ( { setOpen(o); if (!o) setValue('') }}>
Subtotal: ${subtotal.toFixed(2)}
{([ { key: 'percent' as const, label: '% Off' }, { key: 'amount_off' as const, label: '$ Off' }, { key: 'set_price' as const, label: 'Set Total' }, ]).map((m) => ( ))}
setValue(e.target.value)} className="h-9" autoFocus onKeyDown={(e) => { if (e.key === 'Enter') handleApply() }} /> {value && discountAmount > 0 && (
-${discountAmount.toFixed(2)} New total: ${(subtotal - discountAmount).toFixed(2)}
)}
) } // --- Price Adjuster Popover --- type AdjustMode = 'amount_off' | 'set_price' | 'percent' function PriceAdjuster({ item, open, onOpenChange, onApply, isPending }: { item: TransactionLineItem open: boolean onOpenChange: (open: boolean) => void onApply: (amount: number, reason: string) => void isPending: boolean }) { const [mode, setMode] = useState('percent') const [value, setValue] = useState('') const unitPrice = parseFloat(item.unitPrice) const listTotal = unitPrice * item.qty function calculate(): { discountAmount: number; salePrice: number; pct: number } { const v = parseFloat(value) || 0 if (mode === 'amount_off') { const d = Math.min(v, listTotal) return { discountAmount: d, salePrice: listTotal - d, pct: listTotal > 0 ? (d / listTotal) * 100 : 0 } } if (mode === 'set_price') { const d = Math.max(0, listTotal - v) return { discountAmount: d, salePrice: v, pct: listTotal > 0 ? (d / listTotal) * 100 : 0 } } // percent const d = Math.round(listTotal * (v / 100) * 100) / 100 return { discountAmount: d, salePrice: listTotal - d, pct: v } } const calc = calculate() function handleApply() { if (calc.discountAmount <= 0) return const reason = mode === 'percent' ? `${parseFloat(value)}% off` : mode === 'set_price' ? `Price set to $${parseFloat(value).toFixed(2)}` : `$${parseFloat(value).toFixed(2)} off` onApply(calc.discountAmount, reason) setValue('') } return ( { onOpenChange(o); if (!o) setValue('') }}>
List: ${listTotal.toFixed(2)}
{/* Mode tabs */}
{([ { key: 'percent' as const, label: '% Off' }, { key: 'amount_off' as const, label: '$ Off' }, { key: 'set_price' as const, label: 'Set Price' }, ]).map((m) => ( ))}
setValue(e.target.value)} className="h-9" autoFocus onKeyDown={(e) => { if (e.key === 'Enter') handleApply() }} /> {value && parseFloat(value) > 0 && (
Discount -${calc.discountAmount.toFixed(2)} ({calc.pct.toFixed(0)}%)
Sale Price ${calc.salePrice.toFixed(2)}
)}
) }