|
1 | 1 | import { Head, Link, router } from '@inertiajs/react'; |
2 | | -import { Search, Trash2, Plus, Minus, ShoppingCart, ChevronRight, ChevronLeft, UserPlus, ZoomIn, ZoomOut, Home, RefreshCw, Maximize2, Minimize2, Keyboard, CheckCircle, Pencil, FileText, Receipt, X } from 'lucide-react'; |
| 2 | +import { Search, Trash2, Plus, Minus, ShoppingCart, ChevronRight, ChevronLeft, UserPlus, ZoomIn, ZoomOut, Home, RefreshCw, Maximize2, Minimize2, Keyboard, CheckCircle, Pencil, X, Printer } from 'lucide-react'; |
3 | 3 | import { useEffect, useMemo, useRef, useState } from 'react'; |
4 | 4 | import { toast } from 'react-toastify'; |
5 | 5 | import { SearchableSelect } from '@/components/ui/searchable-select'; |
6 | 6 | import { useConfirm } from '@/hooks/use-confirm'; |
7 | 7 | import { cn } from '@/lib/utils'; |
8 | 8 | import { placeOrder as posPlaceOrder } from '@/routes/pos'; |
| 9 | +import { PrintModal, type BillOrder } from '@/components/order-bill'; |
9 | 10 |
|
10 | 11 | type Outlet = { id: number; name: string }; |
11 | 12 | type Customer = { id: number; name: string; phone: string }; |
@@ -115,6 +116,29 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
115 | 116 |
|
116 | 117 | const [itemsModalOrder, setItemsModalOrder] = useState<OrderSummary | null>(null); |
117 | 118 |
|
| 119 | + // Print modal |
| 120 | + const [printOrder, setPrintOrder] = useState<BillOrder | null>(null); |
| 121 | + const [printMode, setPrintMode] = useState<'pos' | 'invoice' | null>(null); |
| 122 | + const [printLoading, setPrintLoading] = useState(false); |
| 123 | + |
| 124 | + async function openPrintModal(order: OrderSummary, mode: 'pos' | 'invoice') { |
| 125 | + setPrintLoading(true); |
| 126 | + try { |
| 127 | + const csrf = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content ?? ''; |
| 128 | + const res = await fetch(`/pos/orders/${order.id}`, { headers: { Accept: 'application/json', 'X-CSRF-TOKEN': csrf } }); |
| 129 | + const data = await res.json(); |
| 130 | + setPrintOrder(data); |
| 131 | + setPrintMode(mode); |
| 132 | + } finally { |
| 133 | + setPrintLoading(false); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + function closePrintModal() { |
| 138 | + setPrintOrder(null); |
| 139 | + setPrintMode(null); |
| 140 | + } |
| 141 | + |
118 | 142 | async function openItemsModal(order: OrderSummary) { |
119 | 143 | setItemsModalOrder(order); |
120 | 144 | setExpandedItems([]); |
@@ -608,6 +632,8 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
608 | 632 | const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target as HTMLElement).isContentEditable; |
609 | 633 |
|
610 | 634 | if (e.key === 'Escape') { |
| 635 | + if (printMode) { closePrintModal(); return; } |
| 636 | + if (itemsModalOrder) { setItemsModalOrder(null); return; } |
611 | 637 | if (showHelp) { setShowHelp(false); return; } |
612 | 638 | if (modalFood) { closeModal(); return; } |
613 | 639 | if (showNewCustomer) { setShowNewCustomer(false); return; } |
@@ -679,7 +705,7 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
679 | 705 |
|
680 | 706 | window.addEventListener('keydown', onKeyDown); |
681 | 707 | return () => window.removeEventListener('keydown', onKeyDown); |
682 | | - }, [showHelp, modalFood, showNewCustomer, search, view, zoom, cart]); |
| 708 | + }, [showHelp, modalFood, showNewCustomer, itemsModalOrder, printMode, search, view, zoom, cart]); |
683 | 709 |
|
684 | 710 | return ( |
685 | 711 | <> |
@@ -918,24 +944,28 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
918 | 944 | </button> |
919 | 945 |
|
920 | 946 | {/* Invoice */} |
921 | | - <Link |
922 | | - href={`/orders/${order.id}`} |
| 947 | + <button |
| 948 | + type="button" |
923 | 949 | title="Invoice" |
924 | | - className="flex flex-col items-center justify-center gap-0.5 py-2.5 text-muted-foreground transition hover:bg-muted dark:hover:bg-stone-800" |
| 950 | + disabled={printLoading} |
| 951 | + onClick={() => openPrintModal(order, 'invoice')} |
| 952 | + className="flex flex-col items-center justify-center gap-0.5 py-2.5 text-muted-foreground transition hover:bg-muted disabled:opacity-40 dark:hover:bg-stone-800" |
925 | 953 | > |
926 | | - <FileText className="h-3.5 w-3.5" /> |
| 954 | + <Printer className="h-3.5 w-3.5" /> |
927 | 955 | <span className="text-[9px] font-bold">Invoice</span> |
928 | | - </Link> |
| 956 | + </button> |
929 | 957 |
|
930 | | - {/* POS Invoice */} |
931 | | - <Link |
932 | | - href={`/orders/${order.id}?print=pos`} |
933 | | - title="POS Invoice" |
934 | | - className="flex flex-col items-center justify-center gap-0.5 py-2.5 text-violet-600 transition hover:bg-violet-50 dark:text-violet-400 dark:hover:bg-violet-900/20" |
| 958 | + {/* POS Bill */} |
| 959 | + <button |
| 960 | + type="button" |
| 961 | + title="POS Bill" |
| 962 | + disabled={printLoading} |
| 963 | + onClick={() => openPrintModal(order, 'pos')} |
| 964 | + className="flex flex-col items-center justify-center gap-0.5 py-2.5 text-violet-600 transition hover:bg-violet-50 disabled:opacity-40 dark:text-violet-400 dark:hover:bg-violet-900/20" |
935 | 965 | > |
936 | | - <Receipt className="h-3.5 w-3.5" /> |
| 966 | + <Printer className="h-3.5 w-3.5" /> |
937 | 967 | <span className="text-[9px] font-bold">POS Bill</span> |
938 | | - </Link> |
| 968 | + </button> |
939 | 969 |
|
940 | 970 | {/* Delete */} |
941 | 971 | <button |
@@ -1592,7 +1622,7 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
1592 | 1622 | <div className="flex items-center justify-between border-b border-border px-5 py-4 dark:border-stone-800"> |
1593 | 1623 | <div> |
1594 | 1624 | <p className="font-headline text-sm font-extrabold text-foreground dark:text-stone-100"> |
1595 | | - {itemsModalOrder.order_number} — Items |
| 1625 | + {itemsModalOrder.order_number} - Items |
1596 | 1626 | </p> |
1597 | 1627 | <p className="mt-0.5 text-[11px] text-muted-foreground capitalize"> |
1598 | 1628 | {itemsModalOrder.status} · {itemsModalOrder.order_type.replace('_', ' ')} |
@@ -1620,7 +1650,7 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
1620 | 1650 | <div className="mb-2 flex items-start justify-between gap-2"> |
1621 | 1651 | <div className="min-w-0"> |
1622 | 1652 | <p className="font-semibold text-foreground dark:text-stone-100"> |
1623 | | - {item.food?.name ?? '—'} |
| 1653 | + {item.food?.name ?? '-'} |
1624 | 1654 | {item.food_variant && ( |
1625 | 1655 | <span className="ml-1 text-xs font-normal text-muted-foreground">({item.food_variant.name})</span> |
1626 | 1656 | )} |
@@ -1679,6 +1709,15 @@ export default function PosIndex({ outlets, categories, foods, customers: initia |
1679 | 1709 |
|
1680 | 1710 | {dialog} |
1681 | 1711 |
|
| 1712 | + {/* ── Print modal ───────────────────────────────────────── */} |
| 1713 | + {printOrder && printMode && ( |
| 1714 | + <PrintModal |
| 1715 | + order={printOrder} |
| 1716 | + mode={printMode} |
| 1717 | + onClose={closePrintModal} |
| 1718 | + /> |
| 1719 | + )} |
| 1720 | + |
1682 | 1721 | </> |
1683 | 1722 | ); |
1684 | 1723 | } |
0 commit comments