@classytic/pos-ui 0.2.0 → 1.3.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 (87) hide show
  1. package/LICENSE +75 -0
  2. package/dist/components/CartSidebar.d.ts +56 -0
  3. package/dist/components/CartSidebar.js +10 -9
  4. package/dist/components/PosTopBar.d.ts +19 -0
  5. package/dist/components/PosTopBar.js +4 -4
  6. package/dist/components/PrinterSettingsDialog.d.ts +8 -0
  7. package/dist/components/PrinterSettingsDialog.js +506 -20
  8. package/dist/components/ReceiptDeliveryDialog.d.ts +11 -0
  9. package/dist/components/ReceiptDeliveryDialog.js +98 -0
  10. package/dist/components/ReceiptReprintDialog.d.ts +10 -0
  11. package/dist/components/ReceiptReprintDialog.js +49 -22
  12. package/dist/components/index.d.ts +6 -0
  13. package/dist/components/index.js +7 -0
  14. package/dist/dashboard/components/CustomerLookupDialog.js +108 -90
  15. package/dist/dashboard/components/CustomerQuickAddDialog.js +3 -1
  16. package/dist/dashboard/components/DeliveryPanel.js +134 -0
  17. package/dist/dashboard/components/ProductCard.js +17 -7
  18. package/dist/dashboard/components/ProductsPanel.js +4 -4
  19. package/dist/dashboard/components/SplitPaymentPanel.js +5 -5
  20. package/dist/dashboard/components/VariantSelectorDialog.js +53 -11
  21. package/dist/dashboard/components/cart/AddChargeDialog.js +3 -2
  22. package/dist/dashboard/components/cart/CartItems.js +5 -5
  23. package/dist/dashboard/components/cart/CartSummary.js +6 -6
  24. package/dist/dashboard/components/cart/CustomerSection.js +3 -3
  25. package/dist/dashboard/components/cart/DiscountSection.js +4 -3
  26. package/dist/dashboard/components/cart/PointsRedemptionSection.js +5 -4
  27. package/dist/dashboard/pos.types.d.ts +32 -0
  28. package/dist/hardware/agent-adapter.js +127 -0
  29. package/dist/hardware/agent-pairing.d.ts +18 -0
  30. package/dist/hardware/agent-pairing.js +63 -0
  31. package/dist/hardware/context.d.ts +1 -5
  32. package/dist/hardware/index.d.ts +4 -3
  33. package/dist/hardware/index.js +3 -2
  34. package/dist/hardware/ports.d.ts +21 -4
  35. package/dist/hardware/tauri-adapter.d.ts +0 -1
  36. package/dist/hardware/tauri-adapter.js +19 -5
  37. package/dist/hardware/web-adapter.d.ts +22 -7
  38. package/dist/hardware/web-adapter.js +56 -14
  39. package/dist/hooks/branch-scoped-key.js +64 -0
  40. package/dist/hooks/index.d.ts +6 -0
  41. package/dist/hooks/index.js +7 -0
  42. package/dist/hooks/sale-attempt.js +62 -0
  43. package/dist/hooks/useManagerAuth.js +9 -1
  44. package/dist/hooks/usePosCart.d.ts +55 -0
  45. package/dist/hooks/usePosCart.js +224 -117
  46. package/dist/hooks/usePosCustomer.d.ts +31 -0
  47. package/dist/hooks/usePosCustomer.js +32 -78
  48. package/dist/hooks/usePosDelivery.d.ts +28 -0
  49. package/dist/hooks/usePosDelivery.js +133 -0
  50. package/dist/hooks/usePosMultiOrder.d.ts +22 -0
  51. package/dist/hooks/usePosMultiOrder.js +14 -4
  52. package/dist/hooks/usePosPayment.d.ts +22 -0
  53. package/dist/lib/delivery-payload.js +83 -0
  54. package/dist/lib/money.js +96 -23
  55. package/dist/node_modules/react-hook-form/dist/index.esm.js +468 -332
  56. package/dist/runtime/auth-port.d.ts +1 -5
  57. package/dist/runtime/branch-port.d.ts +1 -5
  58. package/dist/runtime/config.d.ts +43 -6
  59. package/dist/runtime/config.js +18 -1
  60. package/dist/runtime/index.d.ts +2 -2
  61. package/dist/runtime/index.js +2 -2
  62. package/dist/screens/OrderHistoryDrawer.d.ts +8 -0
  63. package/dist/screens/OrderHistoryDrawer.js +15 -15
  64. package/dist/screens/ParkedOrdersDrawer.d.ts +12 -0
  65. package/dist/screens/ParkedOrdersDrawer.js +3 -3
  66. package/dist/screens/PaymentScreen.d.ts +4 -0
  67. package/dist/screens/PaymentScreen.js +194 -33
  68. package/dist/screens/ProductScreen.d.ts +4 -0
  69. package/dist/screens/ProductScreen.js +13 -10
  70. package/dist/screens/ReceiptScreen.d.ts +4 -0
  71. package/dist/screens/ReceiptScreen.js +96 -72
  72. package/dist/screens/ShiftCloseScreen.d.ts +4 -0
  73. package/dist/screens/ShiftCloseScreen.js +18 -15
  74. package/dist/screens/ShiftOpenScreen.d.ts +4 -0
  75. package/dist/screens/ShiftOpenScreen.js +75 -19
  76. package/dist/screens/index.d.ts +8 -0
  77. package/dist/screens/index.js +9 -0
  78. package/dist/shell/index.d.ts +2 -0
  79. package/dist/shell/pos-shell.d.ts +41 -8
  80. package/dist/shell/pos-shell.js +25 -18
  81. package/dist/state/index.d.ts +3 -0
  82. package/dist/state/index.js +4 -0
  83. package/dist/state/pos-context.d.ts +13 -0
  84. package/dist/state/pos-state.d.ts +49 -0
  85. package/dist/state/pos-state.js +6 -7
  86. package/dist/utils/pos-helpers.js +137 -34
  87. package/package.json +112 -92
@@ -1,23 +1,28 @@
1
1
  "use client";
2
2
 
3
3
  import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { usePosSalesCapabilities } from "../runtime/config.js";
4
5
  import { usePosBranch } from "../runtime/branch-port.js";
6
+ import { formatMajor } from "../lib/money.js";
5
7
  import { usePosContext } from "../state/pos-context.js";
6
8
  import { usePosCart } from "../hooks/usePosCart.js";
7
- import { formatBdt } from "../lib/money.js";
8
9
  import { calculateOrderTotals, isValidBDPhone, parsePositiveNumber, transformOrderToReceipt } from "../utils/pos-helpers.js";
9
10
  import { usePosCustomer } from "../hooks/usePosCustomer.js";
11
+ import { deliveryBlocker, deliveryPayloadOf, isCollectOnDeliverySale, payableTotalOf } from "../lib/delivery-payload.js";
10
12
  import { paymentNeedsReference, usePosPayment } from "../hooks/usePosPayment.js";
13
+ import { usePosDelivery } from "../hooks/usePosDelivery.js";
14
+ import { DeliveryPanel } from "../dashboard/components/DeliveryPanel.js";
11
15
  import { SplitPaymentPanel } from "../dashboard/components/SplitPaymentPanel.js";
12
16
  import { useCallback, useMemo, useState } from "react";
13
17
  import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
14
18
  import { ResponsiveSplitLayout } from "@classytic/fluid/client/core";
15
- import { generateIdempotencyKey, usePosOrders } from "@classytic/commerce-sdk/sales";
19
+ import { usePosOrders } from "@classytic/commerce-sdk/sales";
16
20
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
21
  import { ArrowLeft, Banknote, CreditCard, Loader2, ReceiptText } from "lucide-react";
18
22
  import { Button } from "@/components/ui/button";
19
23
  import { useMembershipConfig } from "@classytic/commerce-sdk/platform";
20
24
  import { toast } from "sonner";
25
+ import { presentError } from "@classytic/fluid/error-contract";
21
26
  import { Input } from "@/components/ui/input";
22
27
  import { Numpad } from "@classytic/fluid/numpad";
23
28
 
@@ -36,11 +41,39 @@ const CASH_SUGGESTIONS = [
36
41
  1e3,
37
42
  2e3
38
43
  ];
44
+ /**
45
+ * Cashier-readable copy for the checkout refusals the till can actually cause.
46
+ *
47
+ * The server's own message is written for a developer reading a log. At a till
48
+ * with a queue behind it, "Tender does not settle the sale: 4500 tendered
49
+ * against 4700 due (underpaid by 200)" is unusable — the cashier needs the
50
+ * ACTION.
51
+ *
52
+ * Only two codes are listed, and both are verified against their source
53
+ * (`pos.tender.mismatch` in `@spinekit/pos`'s errors, `INSUFFICIENT_STOCK` in
54
+ * `@classytic/flow`) — a mapping for a code nothing emits is decoration that
55
+ * reads as coverage. Everything else falls through to the server's own message
56
+ * rather than being flattened into one generic string, because hiding an
57
+ * unexpected failure is worse than an ugly one.
58
+ */
59
+ const CHECKOUT_ERRORS = {
60
+ "pos.tender.mismatch": {
61
+ title: "Payment doesn't match the total",
62
+ message: "Check the amount, or the split entries if you're splitting.",
63
+ retryable: false
64
+ },
65
+ INSUFFICIENT_STOCK: {
66
+ title: "An item is out of stock at this branch",
67
+ message: "Remove it from the cart, or lower the quantity.",
68
+ retryable: false
69
+ }
70
+ };
39
71
  function PaymentScreen() {
40
72
  const token = usePosAuth().getToken() || "";
41
73
  const selectedBranch = usePosBranch();
42
- const { dispatch } = usePosContext();
74
+ const { state, dispatch } = usePosContext();
43
75
  const branchId = selectedBranch?.id;
76
+ const activeRegisterId = state.activeRegisterId;
44
77
  const cart = usePosCart(branchId);
45
78
  const customer = usePosCustomer(token);
46
79
  const { membershipConfig: membership } = useMembershipConfig();
@@ -57,7 +90,31 @@ function PaymentScreen() {
57
90
  customer.selectedCustomer,
58
91
  cart.pointsToRedeemInput
59
92
  ]);
60
- const payment = usePosPayment(token, orderTotals.total);
93
+ const sales = usePosSalesCapabilities();
94
+ const delivery = usePosDelivery(branchId);
95
+ /**
96
+ * WHAT THE CUSTOMER OWES — items minus discounts, PLUS the delivery charge.
97
+ *
98
+ * The server computes `dueMinor` the same way and validates the tender against it,
99
+ * so a screen that quoted only the cart total would collect too little and the sale
100
+ * would be refused at the end of checkout — after the cashier had counted cash.
101
+ * Every payable figure below reads this, not `orderTotals.total`: the numpad target,
102
+ * the split-total check, the single-payment amount, the cash suggestions and both
103
+ * button labels. Leaving any one of them on the cart total reintroduces the gap for
104
+ * exactly one code path, which is the hardest kind to notice.
105
+ *
106
+ * The cart's own total stays visible in the order summary — the customer should see
107
+ * goods and delivery as separate lines.
108
+ */
109
+ const payableTotal = payableTotalOf(orderTotals.total, delivery);
110
+ /**
111
+ * Both conditions, not just the term. `usePosDelivery` already clears the term when
112
+ * the method flips back to carryout, so this is belt-and-braces — but it is read by
113
+ * the RENDER as well as by checkout, and a COD banner on a carryout sale would tell
114
+ * a cashier not to collect money that is owed right now.
115
+ */
116
+ const isCollectOnDelivery = isCollectOnDeliverySale(delivery);
117
+ const payment = usePosPayment(token, payableTotal);
61
118
  const { createOrder, isCreatingOrder } = usePosOrders();
62
119
  const [cashInput, setCashInput] = useState("");
63
120
  useKeyboardShortcut("escape", () => dispatch({ type: "GO_BACK" }), {
@@ -85,8 +142,44 @@ function PaymentScreen() {
85
142
  }, [payment]);
86
143
  const handleCheckout = useCallback(async () => {
87
144
  if (!branchId) return;
145
+ /**
146
+ * The key for THIS sale attempt — minted once and persisted, so tapping
147
+ * "Complete Sale" again after a timeout resends the SAME key and the server
148
+ * replays the order it already created instead of ringing a second one.
149
+ *
150
+ * It is deliberately taken before any validation: an attempt that gets as
151
+ * far as the network must reuse whatever key the previous attempt used, and
152
+ * `beginSaleAttempt` only mints when there is nothing stored.
153
+ */
154
+ const idempotencyKey = cart.beginSaleAttempt();
155
+ if (!idempotencyKey) {
156
+ toast.error("Branch not ready", { description: "The register is still resolving its branch. Try again in a moment." });
157
+ return;
158
+ }
159
+ /**
160
+ * A DELIVERY sale needs a dispatchable address, and the server refuses one that
161
+ * is missing or unusable (`DELIVERY_ADDRESS_UNUSABLE`) before it takes a tender
162
+ * or holds stock. Stopping here means the cashier fixes a field instead of
163
+ * watching a completed-looking checkout come back as an error.
164
+ */
165
+ const blocker = deliveryBlocker(delivery);
166
+ if (blocker) {
167
+ toast.error(blocker);
168
+ return;
169
+ }
88
170
  let paymentPayload = {};
89
- if (payment.state.mode === "split") {
171
+ if (isCollectOnDelivery)
172
+ /**
173
+ * COD takes NO tender — and sends none.
174
+ *
175
+ * Not an empty `payments: []` either: the server refuses a collect-on-delivery
176
+ * sale that also carries a payment, because the two are contradictory and only
177
+ * the cashier knows which was meant. Skipping the whole payment block is also
178
+ * what keeps the cash/reference validation below from demanding money for a
179
+ * sale where none changes hands.
180
+ */
181
+ paymentPayload = {};
182
+ else if (payment.state.mode === "split") {
90
183
  if (!payment.validateAllSplits()) {
91
184
  toast.error("Please fix split payment errors");
92
185
  return;
@@ -97,7 +190,7 @@ function PaymentScreen() {
97
190
  ...entry.reference.trim() && { reference: entry.reference.trim() }
98
191
  }));
99
192
  const splitTotal = payments.reduce((sum, p) => sum + p.amount, 0);
100
- if (Math.abs(splitTotal - orderTotals.total) > .01) {
193
+ if (Math.abs(splitTotal - payableTotal) > .01) {
101
194
  toast.error("Split payment total must match order total");
102
195
  return;
103
196
  }
@@ -120,7 +213,7 @@ function PaymentScreen() {
120
213
  }
121
214
  paymentPayload = { payments: [{
122
215
  method: payment.state.selectedMethod,
123
- amount: orderTotals.total,
216
+ amount: payableTotal,
124
217
  ...paymentNeedsReference(payment.state.selectedMethod) && { reference: payment.state.reference.trim() }
125
218
  }] };
126
219
  }
@@ -141,7 +234,23 @@ function PaymentScreen() {
141
234
  };
142
235
  }),
143
236
  branchId,
144
- deliveryMethod: "pickup",
237
+ ...activeRegisterId && { registerId: activeRegisterId },
238
+ /**
239
+ * The sale's own delivery + payment-term decisions, replacing a hardcoded
240
+ * `"pickup"` that made every till sale a carryout regardless of what the
241
+ * cashier had been told on the phone.
242
+ *
243
+ * `deliveryPrice` is sent only when a fee was actually entered: the server
244
+ * writes canonical `totals.shipping` from it, and an explicit zero would add
245
+ * a priced row for a delivery that costs nothing (a promotion, a loyalty
246
+ * perk) rather than leaving the sale byte-identical to a carryout.
247
+ *
248
+ * `paymentTerm` is sent ALWAYS, prepaid included. The till knows the answer,
249
+ * and a stated fact beats one every downstream reader has to infer from
250
+ * absence — absence is also what an order written before the field existed
251
+ * looks like, and those are different claims.
252
+ */
253
+ ...deliveryPayloadOf(delivery),
145
254
  ...cart.membershipCardId.trim() && { membershipCardId: cart.membershipCardId.trim() },
146
255
  customer: {
147
256
  name: customer.customerName.trim() || "Walk-in Customer",
@@ -151,30 +260,50 @@ function PaymentScreen() {
151
260
  ...paymentPayload,
152
261
  ...orderTotals.manualDiscount > 0 && { discount: orderTotals.manualDiscount },
153
262
  ...orderTotals.pointsRedeemed > 0 && { pointsToRedeem: orderTotals.pointsRedeemed },
154
- idempotencyKey: generateIdempotencyKey()
263
+ idempotencyKey
155
264
  };
156
- const receipt = transformOrderToReceipt(await createOrder(orderData), {
265
+ const response = await createOrder(orderData);
266
+ const branchInfo = {
157
267
  name: selectedBranch?.name || "Store",
158
268
  phone: selectedBranch?.phone
159
- });
269
+ };
270
+ const receipt = transformOrderToReceipt(response, branchInfo);
160
271
  cart.resetCart();
161
272
  payment.reset();
162
273
  customer.resetCustomer();
274
+ /**
275
+ * Delivery too — it is PERSISTED (branch-scoped localStorage), so unlike
276
+ * per-instance state it survives the sale unless cleared.
277
+ *
278
+ * Leaving it would carry one customer's address, phone and delivery charge
279
+ * onto the next customer's sale. That fails in the worst possible direction:
280
+ * the next order looks complete, passes validation, and dispatches goods to
281
+ * the wrong person — and the sale it was copied from was correct, so nothing
282
+ * about either order reads as an error.
283
+ */
284
+ delivery.reset();
163
285
  dispatch({
164
286
  type: "COMPLETE_SALE",
165
287
  receipt
166
288
  });
167
289
  toast.success("Order completed!");
168
290
  } catch (error) {
169
- const message = error instanceof Error ? error.message : "Failed to create order";
170
- toast.error(message);
291
+ const { title, message } = presentError(error, "The sale was not completed. Try again.", {
292
+ codeMap: CHECKOUT_ERRORS,
293
+ fallbackTitle: "Could not complete the sale"
294
+ });
295
+ toast.error(title, { description: message });
171
296
  }
172
297
  }, [
173
298
  branchId,
299
+ activeRegisterId,
174
300
  selectedBranch,
175
301
  cart,
176
302
  customer,
177
303
  payment,
304
+ delivery,
305
+ isCollectOnDelivery,
306
+ payableTotal,
178
307
  orderTotals,
179
308
  createOrder,
180
309
  dispatch
@@ -199,7 +328,7 @@ function PaymentScreen() {
199
328
  variant: "ghost",
200
329
  size: "sm",
201
330
  onClick: () => dispatch({ type: "GO_BACK" }),
202
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Back"]
331
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 me-1" }), "Back"]
203
332
  }), /* @__PURE__ */ jsx("h2", {
204
333
  className: "font-semibold text-lg",
205
334
  children: "Order Summary"
@@ -217,7 +346,7 @@ function PaymentScreen() {
217
346
  children: item.productName
218
347
  }),
219
348
  item.variantLabel && /* @__PURE__ */ jsxs("span", {
220
- className: "text-muted-foreground ml-1",
349
+ className: "text-muted-foreground ms-1",
221
350
  children: [
222
351
  "(",
223
352
  item.variantLabel,
@@ -225,13 +354,13 @@ function PaymentScreen() {
225
354
  ]
226
355
  }),
227
356
  /* @__PURE__ */ jsxs("span", {
228
- className: "text-muted-foreground ml-2",
357
+ className: "text-muted-foreground ms-2",
229
358
  children: ["x", item.quantity]
230
359
  })
231
360
  ]
232
361
  }), /* @__PURE__ */ jsx("span", {
233
- className: "font-medium tabular-nums shrink-0 ml-4",
234
- children: formatBdt(item.lineTotal)
362
+ className: "font-medium tabular-nums shrink-0 ms-4",
363
+ children: formatMajor(item.lineTotal)
235
364
  })]
236
365
  }, `${item.productId}-${item.variantSku || ""}`))
237
366
  }),
@@ -242,14 +371,14 @@ function PaymentScreen() {
242
371
  className: "flex justify-between text-sm",
243
372
  children: [/* @__PURE__ */ jsx("span", { children: "Subtotal" }), /* @__PURE__ */ jsx("span", {
244
373
  className: "tabular-nums",
245
- children: formatBdt(orderTotals.subtotal)
374
+ children: formatMajor(orderTotals.subtotal)
246
375
  })]
247
376
  }),
248
377
  orderTotals.manualDiscount > 0 && /* @__PURE__ */ jsxs("div", {
249
378
  className: "flex justify-between text-sm text-destructive",
250
379
  children: [/* @__PURE__ */ jsx("span", { children: "Discount" }), /* @__PURE__ */ jsxs("span", {
251
380
  className: "tabular-nums",
252
- children: ["-", formatBdt(orderTotals.manualDiscount)]
381
+ children: ["-", formatMajor(orderTotals.manualDiscount)]
253
382
  })]
254
383
  }),
255
384
  orderTotals.tierDiscount > 0 && /* @__PURE__ */ jsxs("div", {
@@ -260,14 +389,28 @@ function PaymentScreen() {
260
389
  ")"
261
390
  ] }), /* @__PURE__ */ jsxs("span", {
262
391
  className: "tabular-nums",
263
- children: ["-", formatBdt(orderTotals.tierDiscount)]
392
+ children: ["-", formatMajor(orderTotals.tierDiscount)]
393
+ })]
394
+ }),
395
+ delivery.fee > 0 && /* @__PURE__ */ jsxs("div", {
396
+ className: "flex justify-between text-sm",
397
+ children: [/* @__PURE__ */ jsx("span", { children: "Delivery" }), /* @__PURE__ */ jsx("span", {
398
+ className: "tabular-nums",
399
+ children: formatMajor(delivery.fee)
264
400
  })]
265
401
  }),
266
402
  /* @__PURE__ */ jsxs("div", {
267
403
  className: "flex justify-between text-lg font-bold pt-2 border-t",
268
404
  children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", {
269
405
  className: "tabular-nums",
270
- children: formatBdt(orderTotals.total)
406
+ children: formatMajor(payableTotal)
407
+ })]
408
+ }),
409
+ isCollectOnDelivery && /* @__PURE__ */ jsxs("div", {
410
+ className: "flex justify-between text-sm font-medium text-amber-600",
411
+ children: [/* @__PURE__ */ jsx("span", { children: "Collect on delivery" }), /* @__PURE__ */ jsx("span", {
412
+ className: "tabular-nums",
413
+ children: formatMajor(payableTotal)
271
414
  })]
272
415
  })
273
416
  ]
@@ -291,16 +434,21 @@ function PaymentScreen() {
291
434
  /* @__PURE__ */ jsxs("div", {
292
435
  className: "flex-1 overflow-y-auto px-6 py-4 space-y-4",
293
436
  children: [
294
- payment.state.mode !== "split" && /* @__PURE__ */ jsx("div", {
437
+ /* @__PURE__ */ jsx(DeliveryPanel, {
438
+ delivery,
439
+ collectOnDeliveryEnabled: sales.collectOnDelivery === true,
440
+ disabled: isCreatingOrder
441
+ }),
442
+ !isCollectOnDelivery && payment.state.mode !== "split" && /* @__PURE__ */ jsx("div", {
295
443
  className: "grid grid-cols-2 gap-2",
296
444
  children: payment.options.map((opt) => /* @__PURE__ */ jsxs(Button, {
297
445
  variant: payment.state.selectedKey === opt.key ? "default" : "outline",
298
446
  className: "h-12 text-sm",
299
447
  onClick: () => payment.selectPayment(opt.key),
300
- children: [opt.key === "cash" ? /* @__PURE__ */ jsx(Banknote, { className: "h-4 w-4 mr-2" }) : /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4 mr-2" }), opt.label]
448
+ children: [opt.key === "cash" ? /* @__PURE__ */ jsx(Banknote, { className: "h-4 w-4 me-2" }) : /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4 me-2" }), opt.label]
301
449
  }, opt.key))
302
450
  }),
303
- /* @__PURE__ */ jsxs("div", {
451
+ !isCollectOnDelivery && /* @__PURE__ */ jsxs("div", {
304
452
  className: "flex gap-2",
305
453
  children: [/* @__PURE__ */ jsx(Button, {
306
454
  variant: payment.state.mode === "single" ? "default" : "outline",
@@ -314,10 +462,23 @@ function PaymentScreen() {
314
462
  children: "Split (F8)"
315
463
  })]
316
464
  }),
317
- payment.state.mode === "split" ? /* @__PURE__ */ jsx(SplitPaymentPanel, {
465
+ isCollectOnDelivery ? /* @__PURE__ */ jsxs("div", {
466
+ className: "rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm dark:border-amber-800 dark:bg-amber-950",
467
+ children: [/* @__PURE__ */ jsx("p", {
468
+ className: "font-medium",
469
+ children: "Cash on delivery"
470
+ }), /* @__PURE__ */ jsxs("p", {
471
+ className: "mt-1 text-muted-foreground",
472
+ children: [
473
+ "Nothing is collected at the till. The order records ",
474
+ formatMajor(payableTotal),
475
+ " as owed, and the courier collects it on delivery."
476
+ ]
477
+ })]
478
+ }) : payment.state.mode === "split" ? /* @__PURE__ */ jsx(SplitPaymentPanel, {
318
479
  entries: payment.state.splitEntries,
319
480
  options: payment.options,
320
- total: orderTotals.total,
481
+ total: payableTotal,
321
482
  remaining: payment.state.splitRemaining,
322
483
  onAdd: payment.addSplit,
323
484
  onUpdate: payment.updateSplit,
@@ -328,9 +489,9 @@ function PaymentScreen() {
328
489
  children: [/* @__PURE__ */ jsx(Button, {
329
490
  variant: "outline",
330
491
  size: "sm",
331
- onClick: () => handleCashSuggestion(Math.ceil(orderTotals.total)),
492
+ onClick: () => handleCashSuggestion(Math.ceil(payableTotal)),
332
493
  children: "Exact"
333
- }), CASH_SUGGESTIONS.filter((s) => s >= orderTotals.total).slice(0, 5).map((amount) => /* @__PURE__ */ jsx(Button, {
494
+ }), CASH_SUGGESTIONS.filter((s) => s >= payableTotal).slice(0, 5).map((amount) => /* @__PURE__ */ jsx(Button, {
334
495
  variant: "outline",
335
496
  size: "sm",
336
497
  onClick: () => handleCashSuggestion(amount),
@@ -344,7 +505,7 @@ function PaymentScreen() {
344
505
  payment.setCashReceived(v);
345
506
  },
346
507
  onSubmit: isCreatingOrder ? void 0 : handleCheckout,
347
- submitLabel: `Pay ${formatBdt(orderTotals.total)}`,
508
+ submitLabel: `Pay ${formatMajor(payableTotal)}`,
348
509
  quickAdd: [
349
510
  100,
350
511
  200,
@@ -361,7 +522,7 @@ function PaymentScreen() {
361
522
  children: "Change"
362
523
  }), /* @__PURE__ */ jsx("p", {
363
524
  className: "text-2xl font-bold text-green-600 tabular-nums",
364
- children: formatBdt(payment.state.changeAmount)
525
+ children: formatMajor(payment.state.changeAmount)
365
526
  })]
366
527
  })
367
528
  ] }) : /* @__PURE__ */ jsxs("div", {
@@ -378,13 +539,13 @@ function PaymentScreen() {
378
539
  })
379
540
  ]
380
541
  }),
381
- (payment.state.mode === "split" || !isCash) && /* @__PURE__ */ jsx("div", {
542
+ (isCollectOnDelivery || payment.state.mode === "split" || !isCash) && /* @__PURE__ */ jsx("div", {
382
543
  className: "px-6 py-4 border-t",
383
544
  children: /* @__PURE__ */ jsx(Button, {
384
545
  className: "w-full h-12 text-base font-semibold",
385
546
  onClick: handleCheckout,
386
547
  disabled: isCreatingOrder,
387
- children: isCreatingOrder ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 mr-2 animate-spin" }), "Processing..."] }) : `Complete Sale ${formatBdt(orderTotals.total)}`
548
+ children: isCreatingOrder ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 me-2 animate-spin" }), "Processing..."] }) : isCollectOnDelivery ? `Place COD order ${formatMajor(payableTotal)}` : `Complete Sale ${formatMajor(payableTotal)}`
388
549
  })
389
550
  })
390
551
  ]
@@ -0,0 +1,4 @@
1
+ //#region src/screens/ProductScreen.d.ts
2
+ declare function ProductScreen(): import("react").JSX.Element;
3
+ //#endregion
4
+ export { ProductScreen };
@@ -50,6 +50,12 @@ function ProductScreen() {
50
50
  const [discountAuthDialogOpen, setDiscountAuthDialogOpen] = useState(false);
51
51
  const cart = usePosCart(branchId);
52
52
  const customer = usePosCustomer(token);
53
+ /**
54
+ * What the cashier typed in the lookup dialog, handed over when they press
55
+ * "Add new". Held here only because two sibling dialogs need it — the lookup
56
+ * owns its own search state, so this is the ONE value that has to cross.
57
+ */
58
+ const [quickAddPrefill, setQuickAddPrefill] = useState({});
53
59
  const multiOrder = usePosMultiOrder(branchId);
54
60
  const [parkedDrawerOpen, setParkedDrawerOpen] = useState(false);
55
61
  const { membershipConfig: membership } = useMembershipConfig();
@@ -67,12 +73,13 @@ function ProductScreen() {
67
73
  customer.selectedCustomer,
68
74
  cart.pointsToRedeemInput
69
75
  ]);
70
- const { products, isLoading: productsLoading, isFetching, refetch } = usePosProducts(branchId, useMemo(() => ({
76
+ const productFilters = useMemo(() => ({
71
77
  search: appliedSearch.trim() || void 0,
72
78
  category: selectedCategory !== "all" ? selectedCategory : void 0,
73
79
  inStockOnly: true,
74
80
  limit: 50
75
- }), [appliedSearch, selectedCategory]), { enabled: !!branchId });
81
+ }), [appliedSearch, selectedCategory]);
82
+ const { products, isLoading: productsLoading, isFetching, refetch } = usePosProducts(branchId, productFilters, { enabled: !!branchId });
76
83
  const lookupMutation = usePosLookupMutation();
77
84
  const { items: categoryTree = [] } = useCategoryTree();
78
85
  const categories = useMemo(() => categoryTree.map((cat) => ({
@@ -283,13 +290,9 @@ function ProductScreen() {
283
290
  /* @__PURE__ */ jsx(CustomerLookupDialog, {
284
291
  open: customer.lookupDialogOpen,
285
292
  onOpenChange: customer.setLookupDialogOpen,
286
- searchValue: customer.customerSearch,
287
- onSearchValueChange: customer.setCustomerSearch,
288
- onSearch: customer.triggerSearch,
289
- results: customer.customerResults,
290
- isLoading: customer.isSearching,
291
293
  onSelect: customer.selectCustomer,
292
- onCreate: () => {
294
+ onCreate: (prefill) => {
295
+ setQuickAddPrefill(prefill);
293
296
  customer.setLookupDialogOpen(false);
294
297
  customer.setCreateDialogOpen(true);
295
298
  }
@@ -298,8 +301,8 @@ function ProductScreen() {
298
301
  token,
299
302
  open: customer.createDialogOpen,
300
303
  onOpenChange: customer.setCreateDialogOpen,
301
- defaultName: customer.customerName,
302
- defaultPhone: isValidPhone(customer.customerSearch.trim()) ? customer.customerSearch.trim() : customer.customerPhone,
304
+ defaultName: quickAddPrefill.name || customer.customerName,
305
+ defaultPhone: quickAddPrefill.phone && isValidPhone(quickAddPrefill.phone) ? quickAddPrefill.phone : customer.customerPhone,
303
306
  onCreated: customer.handleCustomerCreated
304
307
  }),
305
308
  /* @__PURE__ */ jsx(ManagerAuthDialog, {
@@ -0,0 +1,4 @@
1
+ //#region src/screens/ReceiptScreen.d.ts
2
+ declare function ReceiptScreen(): import("react").JSX.Element;
3
+ //#endregion
4
+ export { ReceiptScreen };