@classytic/pos-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +38 -0
  2. package/dist/components/CartSidebar.js +222 -0
  3. package/dist/components/PosTopBar.js +156 -0
  4. package/dist/components/PrinterSettingsDialog.js +163 -0
  5. package/dist/components/ReceiptReprintDialog.js +182 -0
  6. package/dist/dashboard/components/CustomerLookupDialog.js +120 -0
  7. package/dist/dashboard/components/CustomerQuickAddDialog.js +101 -0
  8. package/dist/dashboard/components/ManagerAuthDialog.js +77 -0
  9. package/dist/dashboard/components/ProductCard.js +98 -0
  10. package/dist/dashboard/components/ProductsPanel.js +119 -0
  11. package/dist/dashboard/components/SplitPaymentPanel.js +131 -0
  12. package/dist/dashboard/components/VariantSelectorDialog.js +150 -0
  13. package/dist/dashboard/components/cart/AddChargeDialog.js +99 -0
  14. package/dist/dashboard/components/cart/CartItems.js +103 -0
  15. package/dist/dashboard/components/cart/CartSummary.js +73 -0
  16. package/dist/dashboard/components/cart/CustomerSection.js +127 -0
  17. package/dist/dashboard/components/cart/DiscountSection.js +50 -0
  18. package/dist/dashboard/components/cart/PointsRedemptionSection.js +58 -0
  19. package/dist/hardware/context.d.ts +15 -0
  20. package/dist/hardware/context.js +33 -0
  21. package/dist/hardware/index.d.ts +7 -0
  22. package/dist/hardware/index.js +7 -0
  23. package/dist/hardware/ports.d.ts +116 -0
  24. package/dist/hardware/tauri-adapter.d.ts +7 -0
  25. package/dist/hardware/tauri-adapter.js +77 -0
  26. package/dist/hardware/use-printer-availability.d.ts +12 -0
  27. package/dist/hardware/use-printer-availability.js +50 -0
  28. package/dist/hardware/use-printer-config.d.ts +16 -0
  29. package/dist/hardware/use-printer-config.js +62 -0
  30. package/dist/hardware/web-adapter.d.ts +15 -0
  31. package/dist/hardware/web-adapter.js +80 -0
  32. package/dist/hooks/useManagerAuth.js +83 -0
  33. package/dist/hooks/usePosCart.js +142 -0
  34. package/dist/hooks/usePosCustomer.js +192 -0
  35. package/dist/hooks/usePosMultiOrder.js +48 -0
  36. package/dist/hooks/usePosPayment.js +194 -0
  37. package/dist/lib/cn.js +12 -0
  38. package/dist/lib/loyalty.js +30 -0
  39. package/dist/lib/money.js +41 -0
  40. package/dist/node_modules/react-hook-form/dist/index.esm.js +1661 -0
  41. package/dist/runtime/auth-port.d.ts +46 -0
  42. package/dist/runtime/auth-port.js +21 -0
  43. package/dist/runtime/branch-port.d.ts +38 -0
  44. package/dist/runtime/branch-port.js +26 -0
  45. package/dist/runtime/config.d.ts +24 -0
  46. package/dist/runtime/config.js +21 -0
  47. package/dist/runtime/index.d.ts +4 -0
  48. package/dist/runtime/index.js +5 -0
  49. package/dist/screens/OrderHistoryDrawer.js +245 -0
  50. package/dist/screens/ParkedOrdersDrawer.js +128 -0
  51. package/dist/screens/PaymentScreen.js +397 -0
  52. package/dist/screens/ProductScreen.js +322 -0
  53. package/dist/screens/ReceiptScreen.js +288 -0
  54. package/dist/screens/ShiftCloseScreen.js +365 -0
  55. package/dist/screens/ShiftOpenScreen.js +176 -0
  56. package/dist/shell/index.d.ts +8 -0
  57. package/dist/shell/index.js +5 -0
  58. package/dist/shell/pos-shell.d.ts +23 -0
  59. package/dist/shell/pos-shell.js +132 -0
  60. package/dist/state/pos-context.js +33 -0
  61. package/dist/state/pos-state.js +70 -0
  62. package/dist/utils/customer-display.js +35 -0
  63. package/dist/utils/pos-helpers.js +242 -0
  64. package/package.json +92 -0
  65. package/styles.css +19 -0
@@ -0,0 +1,322 @@
1
+ "use client";
2
+
3
+ import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { usePosBranch } from "../runtime/branch-port.js";
5
+ import { useBarcodeScan } from "../hardware/context.js";
6
+ import { usePosContext } from "../state/pos-context.js";
7
+ import { usePosCart } from "../hooks/usePosCart.js";
8
+ import { calculateOrderTotals } from "../utils/pos-helpers.js";
9
+ import { usePosCustomer } from "../hooks/usePosCustomer.js";
10
+ import { usePosMultiOrder } from "../hooks/usePosMultiOrder.js";
11
+ import { ParkedOrdersDrawer } from "./ParkedOrdersDrawer.js";
12
+ import { useManagerAuth } from "../hooks/useManagerAuth.js";
13
+ import { ProductsPanel } from "../dashboard/components/ProductsPanel.js";
14
+ import { VariantSelectorDialog } from "../dashboard/components/VariantSelectorDialog.js";
15
+ import { CustomerLookupDialog } from "../dashboard/components/CustomerLookupDialog.js";
16
+ import { CustomerQuickAddDialog } from "../dashboard/components/CustomerQuickAddDialog.js";
17
+ import { ManagerAuthDialog } from "../dashboard/components/ManagerAuthDialog.js";
18
+ import { CartSidebar } from "../components/CartSidebar.js";
19
+ import { useCallback, useMemo, useRef, useState } from "react";
20
+ import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
21
+ import { ResponsiveSplitLayout } from "@classytic/fluid/client/core";
22
+ import { usePosLookupMutation, usePosProducts } from "@classytic/commerce-sdk/sales";
23
+ import { jsx, jsxs } from "react/jsx-runtime";
24
+ import { ShoppingBag, ShoppingCart } from "lucide-react";
25
+ import { useCategoryTree } from "@classytic/commerce-sdk/catalog";
26
+ import { useMembershipConfig } from "@classytic/commerce-sdk/platform";
27
+ import { isValidPhone } from "@classytic/commerce-sdk/client";
28
+ import { toast } from "sonner";
29
+
30
+ //#region src/screens/ProductScreen.tsx
31
+ /**
32
+ * ProductScreen — Main selling screen.
33
+ *
34
+ * 65/35 split: product grid (left) + cart sidebar (right).
35
+ * Payment and receipt flow into their own screens via the POS state machine.
36
+ */
37
+ function ProductScreen() {
38
+ const token = usePosAuth().getToken() || "";
39
+ const selectedBranch = usePosBranch();
40
+ const { dispatch } = usePosContext();
41
+ const branchId = selectedBranch?.id;
42
+ const [searchQuery, setSearchQuery] = useState("");
43
+ const [appliedSearch, setAppliedSearch] = useState("");
44
+ const [selectedCategory, setSelectedCategory] = useState("all");
45
+ const [variantSelectorOpen, setVariantSelectorOpen] = useState(false);
46
+ const [selectedProductForVariant, setSelectedProductForVariant] = useState(null);
47
+ const [barcodeInput, setBarcodeInput] = useState("");
48
+ const searchRef = useRef(null);
49
+ const barcodeRef = useRef(null);
50
+ const [discountAuthDialogOpen, setDiscountAuthDialogOpen] = useState(false);
51
+ const cart = usePosCart(branchId);
52
+ const customer = usePosCustomer(token);
53
+ const multiOrder = usePosMultiOrder(branchId);
54
+ const [parkedDrawerOpen, setParkedDrawerOpen] = useState(false);
55
+ const { membershipConfig: membership } = useMembershipConfig();
56
+ const discountAuth = useManagerAuth();
57
+ const orderTotals = useMemo(() => calculateOrderTotals({
58
+ items: cart.cart,
59
+ manualDiscountInput: cart.discountInput,
60
+ membershipConfig: membership,
61
+ customer: customer.selectedCustomer,
62
+ pointsToRedeemInput: cart.pointsToRedeemInput
63
+ }), [
64
+ cart.cart,
65
+ cart.discountInput,
66
+ membership,
67
+ customer.selectedCustomer,
68
+ cart.pointsToRedeemInput
69
+ ]);
70
+ const { products, isLoading: productsLoading, isFetching, refetch } = usePosProducts(branchId, useMemo(() => ({
71
+ search: appliedSearch.trim() || void 0,
72
+ category: selectedCategory !== "all" ? selectedCategory : void 0,
73
+ inStockOnly: true,
74
+ limit: 50
75
+ }), [appliedSearch, selectedCategory]), { enabled: !!branchId });
76
+ const lookupMutation = usePosLookupMutation();
77
+ const { items: categoryTree = [] } = useCategoryTree();
78
+ const categories = useMemo(() => categoryTree.map((cat) => ({
79
+ slug: cat.slug,
80
+ label: cat.name
81
+ })), [categoryTree]);
82
+ useKeyboardShortcut("f3", () => searchRef.current?.focus(), {
83
+ description: "Focus product search",
84
+ category: "Sale"
85
+ });
86
+ useKeyboardShortcut("f4", () => barcodeRef.current?.focus(), {
87
+ description: "Focus barcode input (manual entry)",
88
+ category: "Sale"
89
+ });
90
+ useKeyboardShortcut("f5", () => customer.setLookupDialogOpen(true), {
91
+ description: "Customer lookup",
92
+ category: "Sale"
93
+ });
94
+ useKeyboardShortcut("enter", () => {
95
+ if (cart.cart.length > 0) dispatch({ type: "GO_TO_PAYMENT" });
96
+ }, {
97
+ description: "Go to payment (when cart has items)",
98
+ category: "Sale"
99
+ });
100
+ useKeyboardShortcut("f6", () => handleParkSale(), {
101
+ description: "Park sale (hold for later)",
102
+ category: "Sale"
103
+ });
104
+ useKeyboardShortcut("f7", () => setParkedDrawerOpen(true), {
105
+ description: "Open parked sales",
106
+ category: "Sale"
107
+ });
108
+ const lookupAndAdd = useCallback(async (code) => {
109
+ const trimmed = code.trim();
110
+ if (!trimmed || !branchId) return;
111
+ const result = await lookupMutation.lookup({
112
+ code: trimmed,
113
+ branchId
114
+ });
115
+ if (!result?.product) {
116
+ toast.error("Product not found");
117
+ return;
118
+ }
119
+ const { product: lookupProduct, variantSku } = result;
120
+ const normalizedProduct = products.find((p) => p._id === lookupProduct._id) ?? {
121
+ _id: lookupProduct._id,
122
+ name: lookupProduct.name,
123
+ slug: lookupProduct.slug ?? lookupProduct._id,
124
+ productType: lookupProduct.productType,
125
+ status: "active",
126
+ createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
127
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
128
+ categorySlug: lookupProduct.category,
129
+ images: lookupProduct.images,
130
+ variants: lookupProduct.variants?.map((v) => ({
131
+ ...v,
132
+ isActive: true
133
+ })),
134
+ costPrice: lookupProduct.costPrice
135
+ };
136
+ cart.addToCart(normalizedProduct, variantSku || void 0);
137
+ }, [
138
+ branchId,
139
+ lookupMutation,
140
+ products,
141
+ cart
142
+ ]);
143
+ useBarcodeScan((event) => {
144
+ lookupAndAdd(event.code);
145
+ }, { minCodeLength: 4 });
146
+ const handleBarcodeSubmit = useCallback(async (e) => {
147
+ e.preventDefault();
148
+ const code = barcodeInput;
149
+ setBarcodeInput("");
150
+ await lookupAndAdd(code);
151
+ }, [barcodeInput, lookupAndAdd]);
152
+ const handleOpenVariantSelector = useCallback((product) => {
153
+ setSelectedProductForVariant(product);
154
+ setVariantSelectorOpen(true);
155
+ }, []);
156
+ const handleVariantSelected = useCallback((variantSku) => {
157
+ if (selectedProductForVariant) cart.addToCart(selectedProductForVariant, variantSku);
158
+ }, [selectedProductForVariant, cart]);
159
+ const handleCheckout = useCallback(() => {
160
+ if (cart.cart.length === 0) {
161
+ toast.error("Cart is empty");
162
+ return;
163
+ }
164
+ dispatch({ type: "GO_TO_PAYMENT" });
165
+ }, [cart.cart.length, dispatch]);
166
+ const handleParkSale = useCallback(() => {
167
+ if (cart.cart.length === 0) {
168
+ toast.error("Nothing to park — cart is empty");
169
+ return;
170
+ }
171
+ if (!multiOrder.canPark) {
172
+ toast.error("Maximum 5 parked sales reached. Resume or discard one first.");
173
+ return;
174
+ }
175
+ const customerLabel = customer.customerName.trim() || "Walk-in";
176
+ const itemCount = cart.cart.reduce((sum, it) => sum + it.quantity, 0);
177
+ multiOrder.parkOrder({
178
+ label: `${customerLabel} — ${itemCount} ${itemCount === 1 ? "item" : "items"}`,
179
+ cart: cart.cart,
180
+ customerName: customer.customerName,
181
+ customerPhone: customer.customerPhone,
182
+ membershipCardId: cart.membershipCardId,
183
+ discountInput: cart.discountInput
184
+ });
185
+ cart.resetCart();
186
+ customer.resetCustomer();
187
+ toast.success("Sale parked");
188
+ }, [
189
+ cart,
190
+ customer,
191
+ multiOrder
192
+ ]);
193
+ const handleResumeSale = useCallback((id) => {
194
+ if (cart.cart.length > 0) {
195
+ if (!window.confirm("Replace current cart with the parked sale?")) return;
196
+ }
197
+ const order = multiOrder.resumeOrder(id);
198
+ if (!order) {
199
+ toast.error("Parked sale not found");
200
+ return;
201
+ }
202
+ cart.replaceCart(order.cart);
203
+ cart.setDiscountInput(order.discountInput);
204
+ cart.setMembershipCardId(order.membershipCardId);
205
+ customer.setCustomerName(order.customerName);
206
+ customer.setCustomerPhone(order.customerPhone);
207
+ setParkedDrawerOpen(false);
208
+ toast.success(`Resumed: ${order.label}`);
209
+ }, [
210
+ cart,
211
+ customer,
212
+ multiOrder
213
+ ]);
214
+ return /* @__PURE__ */ jsxs("div", {
215
+ className: "flex flex-col h-full",
216
+ children: [
217
+ /* @__PURE__ */ jsx(ResponsiveSplitLayout, {
218
+ className: "flex-1 min-h-0",
219
+ defaultLayout: [65, 35],
220
+ minSizes: [40, 25],
221
+ persistLayoutKey: "pos-products-cart-split",
222
+ mobileVariant: "tabs",
223
+ mobileTabsPreset: "line",
224
+ rightPanelClassName: "bg-card",
225
+ leftPanel: {
226
+ title: "Products",
227
+ icon: /* @__PURE__ */ jsx(ShoppingBag, { className: "h-4 w-4" }),
228
+ content: /* @__PURE__ */ jsx(ProductsPanel, {
229
+ barcodeInput,
230
+ categories,
231
+ isLookingUp: lookupMutation.isLooking,
232
+ onBarcodeInputChange: setBarcodeInput,
233
+ onBarcodeSubmit: handleBarcodeSubmit,
234
+ onCategoryChange: setSelectedCategory,
235
+ onOpenVariantSelector: handleOpenVariantSelector,
236
+ onProductAdd: cart.addToCart,
237
+ products,
238
+ productsLoading,
239
+ searchQuery,
240
+ selectedCategory,
241
+ onSearchChange: (value) => {
242
+ setSearchQuery(value);
243
+ if (!value.trim()) setAppliedSearch("");
244
+ },
245
+ onSearchSubmit: () => setAppliedSearch(searchQuery)
246
+ })
247
+ },
248
+ rightPanel: {
249
+ title: "Cart",
250
+ icon: /* @__PURE__ */ jsx(ShoppingCart, { className: "h-4 w-4" }),
251
+ content: /* @__PURE__ */ jsx(CartSidebar, {
252
+ cart,
253
+ customer,
254
+ membership,
255
+ orderTotals,
256
+ discountAuth,
257
+ onCheckout: handleCheckout,
258
+ onRequestDiscountAuth: () => setDiscountAuthDialogOpen(true),
259
+ onClearDiscountAuth: () => {
260
+ discountAuth.clearAuth();
261
+ cart.setDiscountInput("");
262
+ },
263
+ onParkSale: handleParkSale,
264
+ onOpenParked: () => setParkedDrawerOpen(true),
265
+ parkedCount: multiOrder.orders.length,
266
+ canPark: multiOrder.canPark
267
+ })
268
+ }
269
+ }),
270
+ /* @__PURE__ */ jsx(ParkedOrdersDrawer, {
271
+ open: parkedDrawerOpen,
272
+ onOpenChange: setParkedDrawerOpen,
273
+ orders: multiOrder.orders,
274
+ onResume: handleResumeSale,
275
+ onDelete: multiOrder.deleteOrder
276
+ }),
277
+ /* @__PURE__ */ jsx(VariantSelectorDialog, {
278
+ product: selectedProductForVariant,
279
+ open: variantSelectorOpen,
280
+ onOpenChange: setVariantSelectorOpen,
281
+ onSelect: handleVariantSelected
282
+ }),
283
+ /* @__PURE__ */ jsx(CustomerLookupDialog, {
284
+ open: customer.lookupDialogOpen,
285
+ onOpenChange: customer.setLookupDialogOpen,
286
+ searchValue: customer.customerSearch,
287
+ onSearchValueChange: customer.setCustomerSearch,
288
+ onSearch: customer.triggerSearch,
289
+ results: customer.customerResults,
290
+ isLoading: customer.isSearching,
291
+ onSelect: customer.selectCustomer,
292
+ onCreate: () => {
293
+ customer.setLookupDialogOpen(false);
294
+ customer.setCreateDialogOpen(true);
295
+ }
296
+ }),
297
+ /* @__PURE__ */ jsx(CustomerQuickAddDialog, {
298
+ token,
299
+ open: customer.createDialogOpen,
300
+ onOpenChange: customer.setCreateDialogOpen,
301
+ defaultName: customer.customerName,
302
+ defaultPhone: isValidPhone(customer.customerSearch.trim()) ? customer.customerSearch.trim() : customer.customerPhone,
303
+ onCreated: customer.handleCustomerCreated
304
+ }),
305
+ /* @__PURE__ */ jsx(ManagerAuthDialog, {
306
+ open: discountAuthDialogOpen,
307
+ onOpenChange: (open) => {
308
+ setDiscountAuthDialogOpen(open);
309
+ if (!open) discountAuth.reset();
310
+ },
311
+ onAuthorize: discountAuth.authorize,
312
+ isPending: discountAuth.isPending,
313
+ error: discountAuth.error,
314
+ title: "Discount Authorization",
315
+ description: "Enter manager or admin credentials to authorize discounts for this sale."
316
+ })
317
+ ]
318
+ });
319
+ }
320
+
321
+ //#endregion
322
+ export { ProductScreen };
@@ -0,0 +1,288 @@
1
+ "use client";
2
+
3
+ import { usePosBranch } from "../runtime/branch-port.js";
4
+ import { RECEIPT_DOM_ID } from "../hardware/web-adapter.js";
5
+ import { useHardware } from "../hardware/context.js";
6
+ import { usePrinterAvailability } from "../hardware/use-printer-availability.js";
7
+ import { usePosContext } from "../state/pos-context.js";
8
+ import { formatPaisa } from "../lib/money.js";
9
+ import { useCallback, useMemo } from "react";
10
+ import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
11
+ import { PrintableView, ResponsiveSplitLayout } from "@classytic/fluid/client/core";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ import { CheckCircle2, Plus, Printer, ReceiptText } from "lucide-react";
14
+ import { Button } from "@/components/ui/button";
15
+ import { getReceiptLocale, paymentMethodLabel } from "@classytic/commerce-sdk/client";
16
+ import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, DocumentParty, DocumentSummary } from "@classytic/fluid/document";
17
+
18
+ //#region src/screens/ReceiptScreen.tsx
19
+ /**
20
+ * ReceiptScreen — Post-checkout receipt with print + new sale.
21
+ *
22
+ * Composed from fluid's `document/*` family:
23
+ * - `<DocumentPage size="receipt">` — 80mm thermal-receipt width, prints
24
+ * via @page CSS in `printDocument()` isolation
25
+ * - `<DocumentHeader>` — branch name, contact, document title/number/date
26
+ * - `<DocumentParty>` — customer block (skipped for walk-ins / pending phones)
27
+ * - `<DocumentLineTable compact>` — line items
28
+ * - `<DocumentSummary>` — subtotal/discount/tax/total + change due
29
+ * - `<DocumentFooter>` — thanks + loyalty notes
30
+ * - `<PrintableView id="pos-receipt">` — print-isolation root
31
+ * - `printDocument("pos-receipt")` — driven by the printer port
32
+ *
33
+ * The `ReceiptPayload` for the printer port is built from the same
34
+ * receipt object so Tauri / agent adapters get a structured render
35
+ * payload (lines, payments, customer, change) when they replace the
36
+ * web stub.
37
+ */
38
+ function isShownPhone(phone) {
39
+ return Boolean(phone && !phone.startsWith("pending_"));
40
+ }
41
+ function buildReceiptPayload(receipt, branchName, branchPhone) {
42
+ const items = receipt.items ?? [];
43
+ const customerName = receipt.customer?.name?.trim() ?? "";
44
+ const showCustomer = customerName.length > 0 && customerName !== "Walk-in Customer";
45
+ const customerPhoneRaw = receipt.customer?.phone ?? null;
46
+ return {
47
+ branch: {
48
+ name: branchName,
49
+ phone: branchPhone
50
+ },
51
+ shift: {
52
+ id: "",
53
+ cashierId: "",
54
+ cashierName: receipt.cashier ?? ""
55
+ },
56
+ order: {
57
+ number: String(receipt.orderNumber ?? ""),
58
+ placedAt: receipt.date ? new Date(receipt.date) : /* @__PURE__ */ new Date(),
59
+ lines: items.map((item) => ({
60
+ name: item.name,
61
+ variant: item.variant,
62
+ qty: item.quantity,
63
+ unitPrice: item.unitPrice ?? 0,
64
+ total: item.total,
65
+ taxRate: item.vatRate,
66
+ taxAmount: item.vatAmount
67
+ })),
68
+ subtotal: receipt.subtotal ?? 0,
69
+ tax: receipt.vat?.amount ?? 0,
70
+ discount: receipt.discount,
71
+ deliveryFee: receipt.deliveryCharge,
72
+ grandTotal: receipt.total ?? 0,
73
+ currency: getReceiptLocale().currency
74
+ },
75
+ payments: receipt.payment ? [{
76
+ method: receipt.payment.method,
77
+ amount: receipt.payment.amount ?? receipt.total ?? 0,
78
+ reference: receipt.payment.reference
79
+ }] : [],
80
+ customer: showCustomer ? {
81
+ name: customerName,
82
+ phone: isShownPhone(customerPhoneRaw) ? customerPhoneRaw : void 0,
83
+ loyaltyId: receipt.membership?.cardId
84
+ } : void 0
85
+ };
86
+ }
87
+ function buildDocumentLines(receipt) {
88
+ return (receipt.items ?? []).map((item, idx) => {
89
+ const subtotal = (item.unitPrice ?? 0) * item.quantity;
90
+ return {
91
+ sequence: idx + 1,
92
+ description: item.name + (item.variant ? ` (${item.variant})` : ""),
93
+ quantity: item.quantity,
94
+ uom: "pcs",
95
+ unitPrice: item.unitPrice ?? 0,
96
+ discountType: "flat",
97
+ discountValue: 0,
98
+ subtotal,
99
+ taxRate: item.vatRate ?? 0,
100
+ taxAmount: item.vatAmount ?? 0,
101
+ lineTotal: item.total,
102
+ formatted: {
103
+ unitPrice: formatPaisa(item.unitPrice ?? 0),
104
+ subtotal: formatPaisa(subtotal),
105
+ taxAmount: formatPaisa(item.vatAmount ?? 0),
106
+ lineTotal: formatPaisa(item.total)
107
+ }
108
+ };
109
+ });
110
+ }
111
+ function ReceiptScreen() {
112
+ const { state, dispatch } = usePosContext();
113
+ const selectedBranch = usePosBranch();
114
+ const hardware = useHardware();
115
+ const { available: printerAvailable, checking: printerChecking } = usePrinterAvailability();
116
+ const receipt = state.lastReceipt;
117
+ const branchName = selectedBranch?.name || receipt?.branch?.name || "Store";
118
+ const branchPhone = selectedBranch?.phone || receipt?.branch?.phone;
119
+ const lines = useMemo(() => receipt ? buildDocumentLines(receipt) : [], [receipt]);
120
+ const handlePrint = useCallback(() => {
121
+ if (!receipt) return;
122
+ hardware.printer.print(buildReceiptPayload(receipt, branchName, branchPhone)).catch((err) => {
123
+ console.warn("[pos] print failed", err);
124
+ });
125
+ }, [
126
+ hardware.printer,
127
+ receipt,
128
+ branchName,
129
+ branchPhone
130
+ ]);
131
+ const handleNewSale = useCallback(() => {
132
+ dispatch({ type: "NEW_SALE" });
133
+ }, [dispatch]);
134
+ useKeyboardShortcut("f2", handleNewSale, {
135
+ description: "New sale",
136
+ category: "Sale"
137
+ });
138
+ useKeyboardShortcut("mod+p", handlePrint, {
139
+ description: "Print receipt",
140
+ category: "Sale"
141
+ });
142
+ if (!receipt) return /* @__PURE__ */ jsx("div", {
143
+ className: "flex items-center justify-center h-full",
144
+ children: /* @__PURE__ */ jsxs("div", {
145
+ className: "text-center",
146
+ children: [/* @__PURE__ */ jsx("p", {
147
+ className: "text-muted-foreground",
148
+ children: "No receipt data available"
149
+ }), /* @__PURE__ */ jsxs(Button, {
150
+ className: "mt-4",
151
+ onClick: handleNewSale,
152
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-2" }), "New Sale"]
153
+ })]
154
+ })
155
+ });
156
+ const customerName = receipt.customer?.name?.trim() ?? "";
157
+ const showCustomer = customerName.length > 0 && customerName !== "Walk-in Customer";
158
+ const customerPhone = isShownPhone(receipt.customer?.phone) ? receipt.customer.phone : null;
159
+ const locale = getReceiptLocale();
160
+ const summaryExtra = receipt.payment ? [{
161
+ label: `Paid (${paymentMethodLabel(receipt.payment.method)})`,
162
+ value: formatPaisa(receipt.payment.amount ?? receipt.total ?? 0),
163
+ muted: true,
164
+ separator: true
165
+ }] : [];
166
+ if (receipt.membership?.pointsEarned && receipt.membership.pointsEarned > 0) summaryExtra.push({
167
+ label: "Points earned",
168
+ value: `+${receipt.membership.pointsEarned}`,
169
+ muted: true,
170
+ separator: false
171
+ });
172
+ return /* @__PURE__ */ jsx(ResponsiveSplitLayout, {
173
+ className: "h-full",
174
+ defaultLayout: [40, 60],
175
+ minSizes: [25, 30],
176
+ persistLayoutKey: "pos-receipt-split",
177
+ mobileVariant: "tabs",
178
+ mobileTabsPreset: "line",
179
+ rightPanelClassName: "bg-muted/30",
180
+ leftPanel: {
181
+ title: "Done",
182
+ icon: /* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4" }),
183
+ content: /* @__PURE__ */ jsxs("div", {
184
+ className: "flex h-full flex-col items-center justify-center gap-6 p-8",
185
+ children: [
186
+ /* @__PURE__ */ jsxs("div", {
187
+ className: "flex flex-col items-center gap-3 text-center",
188
+ children: [
189
+ /* @__PURE__ */ jsx(CheckCircle2, { className: "h-16 w-16 text-green-500" }),
190
+ /* @__PURE__ */ jsx("h2", {
191
+ className: "text-2xl font-bold",
192
+ children: "Sale Complete"
193
+ }),
194
+ receipt.orderNumber && /* @__PURE__ */ jsxs("p", {
195
+ className: "text-muted-foreground",
196
+ children: ["Order #", receipt.orderNumber]
197
+ }),
198
+ /* @__PURE__ */ jsx("p", {
199
+ className: "text-3xl font-bold tabular-nums mt-2",
200
+ children: formatPaisa(receipt.total || 0)
201
+ })
202
+ ]
203
+ }),
204
+ /* @__PURE__ */ jsxs("div", {
205
+ className: "flex flex-col gap-3 w-full max-w-xs",
206
+ children: [/* @__PURE__ */ jsxs(Button, {
207
+ size: "lg",
208
+ className: "h-14 text-lg",
209
+ onClick: handlePrint,
210
+ disabled: !printerAvailable && !printerChecking,
211
+ title: printerChecking ? "Checking printer..." : printerAvailable ? "Print receipt (Ctrl+P)" : "No printer reachable — set POS_PRINTER_HOST or check network",
212
+ children: [/* @__PURE__ */ jsx(Printer, { className: "h-5 w-5 mr-2" }), "Print Receipt"]
213
+ }), /* @__PURE__ */ jsxs(Button, {
214
+ size: "lg",
215
+ variant: "outline",
216
+ className: "h-14 text-lg",
217
+ onClick: handleNewSale,
218
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-5 w-5 mr-2" }), "New Sale (F2)"]
219
+ })]
220
+ }),
221
+ /* @__PURE__ */ jsx("p", {
222
+ className: "text-xs text-muted-foreground mt-4",
223
+ children: printerAvailable ? "Ctrl+P to print" : "No printer reachable"
224
+ })
225
+ ]
226
+ })
227
+ },
228
+ rightPanel: {
229
+ title: "Receipt",
230
+ icon: /* @__PURE__ */ jsx(ReceiptText, { className: "h-4 w-4" }),
231
+ content: /* @__PURE__ */ jsx("div", {
232
+ className: "flex h-full items-start justify-center overflow-y-auto p-8",
233
+ children: /* @__PURE__ */ jsx(PrintableView, {
234
+ id: RECEIPT_DOM_ID,
235
+ className: "bg-white dark:bg-card shadow-lg",
236
+ children: /* @__PURE__ */ jsxs(DocumentPage, {
237
+ size: "receipt",
238
+ children: [
239
+ /* @__PURE__ */ jsx(DocumentHeader, {
240
+ companyName: branchName,
241
+ companyContact: branchPhone,
242
+ ...receipt.vat?.sellerBin ? { companyTaxId: `${locale.taxIdLabel}: ${receipt.vat.sellerBin}` } : {},
243
+ documentTitle: "Receipt",
244
+ documentNumber: receipt.orderNumber,
245
+ date: receipt.date ?? ""
246
+ }),
247
+ showCustomer && /* @__PURE__ */ jsx(DocumentParty, {
248
+ billTo: {
249
+ name: customerName,
250
+ ...customerPhone ? { contact: customerPhone } : {},
251
+ ...receipt.membership?.cardId ? { extra: { Loyalty: receipt.membership.cardId } } : {}
252
+ },
253
+ billToLabel: "Customer"
254
+ }),
255
+ /* @__PURE__ */ jsx(DocumentLineTable, {
256
+ lines,
257
+ columns: [
258
+ "description",
259
+ "quantity",
260
+ "lineTotal"
261
+ ],
262
+ showSequence: false,
263
+ compact: true
264
+ }),
265
+ /* @__PURE__ */ jsx(DocumentSummary, {
266
+ subtotal: formatPaisa(receipt.subtotal ?? 0),
267
+ ...receipt.discount && receipt.discount > 0 ? { discount: formatPaisa(receipt.discount) } : {},
268
+ ...receipt.vat?.amount && receipt.vat.amount > 0 ? { tax: formatPaisa(receipt.vat.amount) } : {},
269
+ total: formatPaisa(receipt.total ?? 0),
270
+ extraRows: summaryExtra,
271
+ currency: locale.currency,
272
+ labels: { tax: locale.taxLabel },
273
+ width: "w-full"
274
+ }),
275
+ /* @__PURE__ */ jsx(DocumentFooter, { notes: /* @__PURE__ */ jsx("p", {
276
+ className: "text-center",
277
+ children: locale.thankYouText
278
+ }) })
279
+ ]
280
+ })
281
+ })
282
+ })
283
+ }
284
+ });
285
+ }
286
+
287
+ //#endregion
288
+ export { ReceiptScreen };