@classytic/pos-ui 0.2.0 → 1.2.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,19 +1,24 @@
1
1
  "use client";
2
2
 
3
3
  import { usePosBranch } from "../runtime/branch-port.js";
4
- import { RECEIPT_DOM_ID } from "../hardware/web-adapter.js";
4
+ import { formatMinor } from "../lib/money.js";
5
+ import { RECEIPT_DOM_ID, printViaBrowserWindow } from "../hardware/web-adapter.js";
5
6
  import { useHardware } from "../hardware/context.js";
6
7
  import { usePrinterAvailability } from "../hardware/use-printer-availability.js";
7
8
  import { usePosContext } from "../state/pos-context.js";
8
- import { formatPaisa } from "../lib/money.js";
9
+ import { buildReceiptPayload } from "../utils/pos-helpers.js";
9
10
  import { useCallback, useMemo } from "react";
10
11
  import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
11
12
  import { PrintableView, ResponsiveSplitLayout } from "@classytic/fluid/client/core";
13
+ import { DEFAULT_POS_RECEIPT_CONFIG } from "@classytic/commerce-sdk/sales";
12
14
  import { jsx, jsxs } from "react/jsx-runtime";
13
15
  import { CheckCircle2, Plus, Printer, ReceiptText } from "lucide-react";
14
16
  import { Button } from "@/components/ui/button";
17
+ import { useEffectiveConfig, usePlatformIdentity } from "@classytic/commerce-sdk/platform";
15
18
  import { getReceiptLocale, paymentMethodLabel } from "@classytic/commerce-sdk/client";
16
- import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, DocumentParty, DocumentSummary } from "@classytic/fluid/document";
19
+ import { toast } from "sonner";
20
+ import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, DocumentParty, DocumentQR, DocumentSummary } from "@classytic/fluid/document";
21
+ import { renderSVG } from "uqr";
17
22
 
18
23
  //#region src/screens/ReceiptScreen.tsx
19
24
  /**
@@ -38,51 +43,18 @@ import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, Docume
38
43
  function isShownPhone(phone) {
39
44
  return Boolean(phone && !phone.startsWith("pending_"));
40
45
  }
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
- };
46
+ /** Flatten a branch address to a single presentational line for the letterhead. */
47
+ function formatAddressLine(a) {
48
+ if (!a) return void 0;
49
+ const parts = [
50
+ a.line1,
51
+ a.line2,
52
+ a.city,
53
+ a.state,
54
+ a.postalCode,
55
+ a.country
56
+ ].filter(Boolean);
57
+ return parts.length ? parts.join(", ") : void 0;
86
58
  }
87
59
  function buildDocumentLines(receipt) {
88
60
  return (receipt.items ?? []).map((item, idx) => {
@@ -100,10 +72,10 @@ function buildDocumentLines(receipt) {
100
72
  taxAmount: item.vatAmount ?? 0,
101
73
  lineTotal: item.total,
102
74
  formatted: {
103
- unitPrice: formatPaisa(item.unitPrice ?? 0),
104
- subtotal: formatPaisa(subtotal),
105
- taxAmount: formatPaisa(item.vatAmount ?? 0),
106
- lineTotal: formatPaisa(item.total)
75
+ unitPrice: formatMinor(item.unitPrice ?? 0),
76
+ subtotal: formatMinor(subtotal),
77
+ taxAmount: formatMinor(item.vatAmount ?? 0),
78
+ lineTotal: formatMinor(item.total)
107
79
  }
108
80
  };
109
81
  });
@@ -114,19 +86,47 @@ function ReceiptScreen() {
114
86
  const hardware = useHardware();
115
87
  const { available: printerAvailable, checking: printerChecking } = usePrinterAvailability();
116
88
  const receipt = state.lastReceipt;
117
- const branchName = selectedBranch?.name || receipt?.branch?.name || "Store";
89
+ const { identity } = usePlatformIdentity();
90
+ const { effective: receiptCfgEff } = useEffectiveConfig("pos.receipt", { branchId: selectedBranch?.id });
91
+ const rcfg = receiptCfgEff?.value ?? DEFAULT_POS_RECEIPT_CONFIG;
118
92
  const branchPhone = selectedBranch?.phone || receipt?.branch?.phone;
93
+ const companyLogo = identity?.company.logoUrl;
94
+ const companyName = identity?.branch?.name || selectedBranch?.name || receipt?.branch?.name || identity?.company.name || "Store";
95
+ const companyAddress = formatAddressLine(identity?.branch?.address);
96
+ const companyContact = identity?.branch?.phone || branchPhone || identity?.company.supportPhone;
119
97
  const lines = useMemo(() => receipt ? buildDocumentLines(receipt) : [], [receipt]);
98
+ const orderRef = receipt?.orderNumber != null ? String(receipt.orderNumber) : "";
99
+ const qrSvg = useMemo(() => {
100
+ if (!rcfg.showQr || !orderRef) return null;
101
+ const value = rcfg.qrUrlTemplate ? rcfg.qrUrlTemplate.replace("{orderNumber}", orderRef) : orderRef;
102
+ try {
103
+ return renderSVG(value);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }, [
108
+ rcfg.showQr,
109
+ rcfg.qrUrlTemplate,
110
+ orderRef
111
+ ]);
120
112
  const handlePrint = useCallback(() => {
121
113
  if (!receipt) return;
122
- hardware.printer.print(buildReceiptPayload(receipt, branchName, branchPhone)).catch((err) => {
114
+ hardware.printer.print(buildReceiptPayload(receipt, companyName, companyContact)).then((outcome) => {
115
+ if (outcome === "confirmed") return;
116
+ toast.error(outcome === "unknown" ? "Printer status unknown — it may have already printed." : "Couldn't reach the printer.", { action: {
117
+ label: "Print via browser",
118
+ onClick: () => {
119
+ printViaBrowserWindow();
120
+ }
121
+ } });
122
+ }).catch((err) => {
123
123
  console.warn("[pos] print failed", err);
124
124
  });
125
125
  }, [
126
126
  hardware.printer,
127
127
  receipt,
128
- branchName,
129
- branchPhone
128
+ companyName,
129
+ companyContact
130
130
  ]);
131
131
  const handleNewSale = useCallback(() => {
132
132
  dispatch({ type: "NEW_SALE" });
@@ -149,7 +149,7 @@ function ReceiptScreen() {
149
149
  }), /* @__PURE__ */ jsxs(Button, {
150
150
  className: "mt-4",
151
151
  onClick: handleNewSale,
152
- children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-2" }), "New Sale"]
152
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 me-2" }), "New Sale"]
153
153
  })]
154
154
  })
155
155
  });
@@ -159,7 +159,7 @@ function ReceiptScreen() {
159
159
  const locale = getReceiptLocale();
160
160
  const summaryExtra = receipt.payment ? [{
161
161
  label: `Paid (${paymentMethodLabel(receipt.payment.method)})`,
162
- value: formatPaisa(receipt.payment.amount ?? receipt.total ?? 0),
162
+ value: formatMinor(receipt.payment.amount ?? receipt.total ?? 0),
163
163
  muted: true,
164
164
  separator: true
165
165
  }] : [];
@@ -197,7 +197,7 @@ function ReceiptScreen() {
197
197
  }),
198
198
  /* @__PURE__ */ jsx("p", {
199
199
  className: "text-3xl font-bold tabular-nums mt-2",
200
- children: formatPaisa(receipt.total || 0)
200
+ children: formatMinor(receipt.total || 0)
201
201
  })
202
202
  ]
203
203
  }),
@@ -209,13 +209,13 @@ function ReceiptScreen() {
209
209
  onClick: handlePrint,
210
210
  disabled: !printerAvailable && !printerChecking,
211
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"]
212
+ children: [/* @__PURE__ */ jsx(Printer, { className: "h-5 w-5 me-2" }), "Print Receipt"]
213
213
  }), /* @__PURE__ */ jsxs(Button, {
214
214
  size: "lg",
215
215
  variant: "outline",
216
216
  className: "h-14 text-lg",
217
217
  onClick: handleNewSale,
218
- children: [/* @__PURE__ */ jsx(Plus, { className: "h-5 w-5 mr-2" }), "New Sale (F2)"]
218
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-5 w-5 me-2" }), "New Sale (F2)"]
219
219
  })]
220
220
  }),
221
221
  /* @__PURE__ */ jsx("p", {
@@ -237,13 +237,23 @@ function ReceiptScreen() {
237
237
  size: "receipt",
238
238
  children: [
239
239
  /* @__PURE__ */ jsx(DocumentHeader, {
240
- companyName: branchName,
241
- companyContact: branchPhone,
242
- ...receipt.vat?.sellerBin ? { companyTaxId: `${locale.taxIdLabel}: ${receipt.vat.sellerBin}` } : {},
240
+ ...companyLogo && rcfg.showLogo ? { logo: companyLogo } : {},
241
+ companyName,
242
+ ...companyAddress ? { companyAddress } : {},
243
+ ...companyContact ? { companyContact } : {},
244
+ ...receipt.vat?.sellerBin ? { companyTaxId: `${locale.taxIdLabel}: ${receipt.vat.sellerBin}` } : identity?.company.taxId ? { companyTaxId: `${locale.taxIdLabel}: ${identity.company.taxId}` } : {},
243
245
  documentTitle: "Receipt",
244
246
  documentNumber: receipt.orderNumber,
245
247
  date: receipt.date ?? ""
246
248
  }),
249
+ rcfg.headerMessage && /* @__PURE__ */ jsx("p", {
250
+ className: "whitespace-pre-line text-center text-xs text-muted-foreground",
251
+ children: rcfg.headerMessage
252
+ }),
253
+ rcfg.showCashier && receipt.cashier && /* @__PURE__ */ jsxs("p", {
254
+ className: "text-xs text-muted-foreground",
255
+ children: ["Served by: ", receipt.cashier]
256
+ }),
247
257
  showCustomer && /* @__PURE__ */ jsx(DocumentParty, {
248
258
  billTo: {
249
259
  name: customerName,
@@ -254,7 +264,7 @@ function ReceiptScreen() {
254
264
  }),
255
265
  /* @__PURE__ */ jsx(DocumentLineTable, {
256
266
  lines,
257
- columns: [
267
+ columns: rcfg.basicReceipt ? ["description", "quantity"] : [
258
268
  "description",
259
269
  "quantity",
260
270
  "lineTotal"
@@ -262,19 +272,33 @@ function ReceiptScreen() {
262
272
  showSequence: false,
263
273
  compact: true
264
274
  }),
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),
275
+ !rcfg.basicReceipt && /* @__PURE__ */ jsx(DocumentSummary, {
276
+ subtotal: formatMinor(receipt.subtotal ?? 0),
277
+ ...receipt.discount && receipt.discount > 0 ? { discount: formatMinor(receipt.discount) } : {},
278
+ ...receipt.vat?.amount && receipt.vat.amount > 0 ? { tax: formatMinor(receipt.vat.amount) } : {},
279
+ total: formatMinor(receipt.total ?? 0),
270
280
  extraRows: summaryExtra,
271
281
  currency: locale.currency,
272
282
  labels: { tax: locale.taxLabel },
273
283
  width: "w-full"
274
284
  }),
275
- /* @__PURE__ */ jsx(DocumentFooter, { notes: /* @__PURE__ */ jsx("p", {
285
+ qrSvg && /* @__PURE__ */ jsx("div", {
286
+ className: "mt-3 flex justify-center",
287
+ children: /* @__PURE__ */ jsx(DocumentQR, {
288
+ size: "w-28",
289
+ label: rcfg.qrUrlTemplate ? "Scan for your invoice" : `Order ${orderRef}`,
290
+ children: /* @__PURE__ */ jsx("span", {
291
+ className: "block w-full [&>svg]:h-full [&>svg]:w-full",
292
+ dangerouslySetInnerHTML: { __html: qrSvg }
293
+ })
294
+ })
295
+ }),
296
+ /* @__PURE__ */ jsx(DocumentFooter, { notes: /* @__PURE__ */ jsxs("div", {
276
297
  className: "text-center",
277
- children: locale.thankYouText
298
+ children: [/* @__PURE__ */ jsx("p", { children: locale.thankYouText }), rcfg.footerMessage && /* @__PURE__ */ jsx("p", {
299
+ className: "mt-1 whitespace-pre-line text-muted-foreground",
300
+ children: rcfg.footerMessage
301
+ })]
278
302
  }) })
279
303
  ]
280
304
  })
@@ -0,0 +1,4 @@
1
+ //#region src/screens/ShiftCloseScreen.d.ts
2
+ declare function ShiftCloseScreen(): import("react").JSX.Element;
3
+ //#endregion
4
+ export { ShiftCloseScreen };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { formatMinor, majorToMinor } from "../lib/money.js";
4
5
  import { usePosContext } from "../state/pos-context.js";
5
- import { formatPaisa } from "../lib/money.js";
6
6
  import { useManagerAuth } from "../hooks/useManagerAuth.js";
7
7
  import { ManagerAuthDialog } from "../dashboard/components/ManagerAuthDialog.js";
8
8
  import { useCallback, useMemo, useState } from "react";
@@ -10,6 +10,7 @@ import { useBlindCloseShift, useCloseShift, usePosCurrentShift } from "@classyti
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { AlertCircle, AlertTriangle, ArrowLeft, CheckCircle2, ClipboardList, ShieldCheck } from "lucide-react";
12
12
  import { Button } from "@/components/ui/button";
13
+ import { presentError } from "@classytic/fluid/error-contract";
13
14
  import { Input } from "@/components/ui/input";
14
15
  import { Label } from "@/components/ui/label";
15
16
  import { Numpad } from "@classytic/fluid/numpad";
@@ -62,7 +63,7 @@ function ShiftCloseScreen() {
62
63
  const expectedCash = shift?.expectedCash ?? liveExpectedCash;
63
64
  const salesCount = shift?.salesCount ?? 0;
64
65
  const salesTotal = shift?.salesTotal ?? 0;
65
- const actualCash = Math.round((parseFloat(closingCash) || 0) * 100);
66
+ const actualCash = majorToMinor(parseFloat(closingCash) || 0);
66
67
  const difference = closingCash ? actualCash - expectedCash : 0;
67
68
  const varianceAbs = Math.abs(difference);
68
69
  const policy = shift?.policySnapshot;
@@ -124,9 +125,9 @@ function ShiftCloseScreen() {
124
125
  });
125
126
  dispatch({ type: "CONFIRM_CLOSE" });
126
127
  } catch (err) {
127
- const msg = err.message || "Failed to close register";
128
- setSubmitError(msg);
129
- if (msg.toLowerCase().includes("variance")) setAuthDialogOpen(true);
128
+ const { message } = presentError(err, "The register was not closed. Try again.");
129
+ setSubmitError(message);
130
+ if ((err instanceof Error ? err.message : "").toLowerCase().includes("variance")) setAuthDialogOpen(true);
130
131
  }
131
132
  }, [
132
133
  shift,
@@ -157,6 +158,8 @@ function ShiftCloseScreen() {
157
158
  supervisorReady,
158
159
  handleSubmit
159
160
  ]);
161
+ const closeMutationErr = closeErr ?? blindErr;
162
+ const closeFailure = closeMutationErr ? presentError(closeMutationErr, "The register was not closed. Try again.") : null;
160
163
  return /* @__PURE__ */ jsxs("div", {
161
164
  className: "flex items-center justify-center h-full bg-muted/30",
162
165
  children: [/* @__PURE__ */ jsxs("div", {
@@ -169,12 +172,12 @@ function ShiftCloseScreen() {
169
172
  variant: "ghost",
170
173
  size: "sm",
171
174
  onClick: handleCancel,
172
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Cancel"]
175
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 me-1" }), "Cancel"]
173
176
  }),
174
177
  /* @__PURE__ */ jsxs("h1", {
175
178
  className: "text-xl font-bold flex items-center gap-2",
176
179
  children: [/* @__PURE__ */ jsx(ClipboardList, { className: "h-5 w-5" }), /* @__PURE__ */ jsxs("span", { children: ["Closing Shift", shift?.openedAt ? /* @__PURE__ */ jsxs("span", {
177
- className: "text-sm font-medium text-muted-foreground ml-2",
180
+ className: "text-sm font-medium text-muted-foreground ms-2",
178
181
  children: [
179
182
  "· opened",
180
183
  " ",
@@ -208,7 +211,7 @@ function ShiftCloseScreen() {
208
211
  children: "Revenue"
209
212
  }), /* @__PURE__ */ jsx("p", {
210
213
  className: "text-lg font-bold tabular-nums",
211
- children: formatPaisa(salesTotal)
214
+ children: formatMinor(salesTotal)
212
215
  })]
213
216
  }),
214
217
  /* @__PURE__ */ jsxs("div", {
@@ -218,7 +221,7 @@ function ShiftCloseScreen() {
218
221
  children: "Expected Cash"
219
222
  }), /* @__PURE__ */ jsx("p", {
220
223
  className: "text-lg font-bold tabular-nums",
221
- children: formatPaisa(expectedCash)
224
+ children: formatMinor(expectedCash)
222
225
  })]
223
226
  })
224
227
  ]
@@ -245,7 +248,7 @@ function ShiftCloseScreen() {
245
248
  children: "Counted"
246
249
  }), /* @__PURE__ */ jsx("p", {
247
250
  className: "text-3xl font-bold tabular-nums mt-1",
248
- children: closingCash ? formatPaisa(actualCash) : "—"
251
+ children: closingCash ? formatMinor(actualCash) : "—"
249
252
  })]
250
253
  }),
251
254
  /* @__PURE__ */ jsx(Numpad, {
@@ -268,7 +271,7 @@ function ShiftCloseScreen() {
268
271
  })]
269
272
  }), /* @__PURE__ */ jsxs("p", {
270
273
  className: "text-2xl font-bold tabular-nums",
271
- children: [difference >= 0 ? "+" : "", formatPaisa(difference)]
274
+ children: [difference >= 0 ? "+" : "", formatMinor(difference)]
272
275
  })]
273
276
  }),
274
277
  overrideRequired && !cashierIsSupervisor && !blindCloseRequired && /* @__PURE__ */ jsx("div", {
@@ -284,7 +287,7 @@ function ShiftCloseScreen() {
284
287
  className: "text-xs text-amber-800 dark:text-amber-200 mt-0.5",
285
288
  children: [
286
289
  "Variance exceeds the branch threshold",
287
- policy ? ` (${formatPaisa(policy.varianceThresholdAbs)} or ${policy.varianceThresholdPct}%)` : "",
290
+ policy ? ` (${formatMinor(policy.varianceThresholdAbs)} or ${policy.varianceThresholdPct}%)` : "",
288
291
  ". A supervisor must authenticate before closing."
289
292
  ]
290
293
  })]
@@ -295,7 +298,7 @@ function ShiftCloseScreen() {
295
298
  className: "w-full bg-amber-600 hover:bg-amber-700 text-white",
296
299
  onClick: () => setAuthDialogOpen(true),
297
300
  disabled: isPending,
298
- children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 mr-2" }), "Get supervisor authorization"]
301
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 me-2" }), "Get supervisor authorization"]
299
302
  })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
300
303
  /* @__PURE__ */ jsxs("div", {
301
304
  className: "flex items-start gap-2",
@@ -340,9 +343,9 @@ function ShiftCloseScreen() {
340
343
  })
341
344
  ] })
342
345
  }),
343
- (submitError || closeErr || blindErr) && /* @__PURE__ */ jsxs("div", {
346
+ (submitError || closeFailure) && /* @__PURE__ */ jsxs("div", {
344
347
  className: "flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200",
345
- children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError || closeErr?.message || blindErr?.message })]
348
+ children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError ?? closeFailure?.message })]
346
349
  }),
347
350
  blindCloseRequired && /* @__PURE__ */ jsx("p", {
348
351
  className: "text-xs text-center text-muted-foreground",
@@ -0,0 +1,4 @@
1
+ //#region src/screens/ShiftOpenScreen.d.ts
2
+ declare function ShiftOpenScreen(): import("react").JSX.Element;
3
+ //#endregion
4
+ export { ShiftOpenScreen };
@@ -3,13 +3,14 @@
3
3
  import { usePosAuth } from "../runtime/auth-port.js";
4
4
  import { usePosRuntime } from "../runtime/config.js";
5
5
  import { usePosBranch } from "../runtime/branch-port.js";
6
+ import { formatMajor, formatMinor, majorToMinor } from "../lib/money.js";
6
7
  import { usePosContext } from "../state/pos-context.js";
7
- import { formatBdt, formatPaisa } from "../lib/money.js";
8
- import { useCallback, useMemo, useState } from "react";
9
- import { useOpenShift, usePosCurrentShift } from "@classytic/commerce-sdk/sales";
8
+ import { useCallback, useEffect, useMemo, useState } from "react";
9
+ import { useOpenShift, usePosCurrentShift, useRegisters } from "@classytic/commerce-sdk/sales";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
- import { AlertCircle, DollarSign, LayoutDashboard, LogOut, Store } from "lucide-react";
11
+ import { AlertCircle, DollarSign, LayoutDashboard, LogOut, Monitor, Store } from "lucide-react";
12
12
  import { Button } from "@/components/ui/button";
13
+ import { presentError } from "@classytic/fluid/error-contract";
13
14
  import { Numpad } from "@classytic/fluid/numpad";
14
15
 
15
16
  //#region src/screens/ShiftOpenScreen.tsx
@@ -19,6 +20,17 @@ import { Numpad } from "@classytic/fluid/numpad";
19
20
  * Wired to backend via `useOpenShift`. On success the POS state machine
20
21
  * dispatches OPEN_SHIFT and the pos-shell fast-forwards to products.
21
22
  */
23
+ /**
24
+ * The one open-shift refusal whose server message names an internal id
25
+ * ("An active shift already exists for scope 'ynAb…'") — verified against
26
+ * `ActiveShiftAlreadyOpenError` in `@classytic/pos`. Every other POS shift code
27
+ * carries a message a cashier can act on, so it is left to fall through.
28
+ */
29
+ const SHIFT_OPEN_ERRORS = { ACTIVE_SHIFT_ALREADY_OPEN: {
30
+ title: "A register is already open",
31
+ message: "Close the open shift before starting a new one.",
32
+ retryable: false
33
+ } };
22
34
  function ShiftOpenScreen() {
23
35
  const { dispatch } = usePosContext();
24
36
  const branch = usePosBranch();
@@ -26,32 +38,53 @@ function ShiftOpenScreen() {
26
38
  const { onNavigate } = usePosRuntime();
27
39
  const { shift: existingShift, isLoading: isLoadingShift } = usePosCurrentShift();
28
40
  const { openShift, isPending, error } = useOpenShift();
41
+ const { items: registers = [], isLoading: isLoadingRegisters } = useRegisters();
29
42
  const [openingCash, setOpeningCash] = useState("");
30
43
  const [submitError, setSubmitError] = useState(null);
44
+ const [selectedRegisterId, setSelectedRegisterId] = useState(null);
45
+ const activeRegisters = useMemo(() => registers.filter((r) => r.isActive), [registers]);
46
+ const needsRegisterChoice = activeRegisters.length > 1;
47
+ useEffect(() => {
48
+ if (activeRegisters.length === 1 && !selectedRegisterId) setSelectedRegisterId(activeRegisters[0]?._id ?? null);
49
+ }, [activeRegisters, selectedRegisterId]);
50
+ const canOpen = !needsRegisterChoice || !!selectedRegisterId;
31
51
  const userName = sessionUser?.name || "Cashier";
32
52
  const branchName = branch?.name || "POS Terminal";
33
53
  const requiredFloat = existingShift?.policySnapshot?.requiredOpeningFloat ?? null;
34
54
  const submitLabel = useMemo(() => {
35
55
  if (isPending) return "Opening…";
36
- if (requiredFloat !== null) return `Open with ${formatPaisa(requiredFloat)}`;
56
+ if (requiredFloat !== null) return `Open with ${formatMinor(requiredFloat)}`;
37
57
  return "Open Register";
38
58
  }, [isPending, requiredFloat]);
39
59
  const handleOpenShift = useCallback(async () => {
40
60
  setSubmitError(null);
41
- const amount = Math.round((parseFloat(openingCash) || 0) * 100);
61
+ if (needsRegisterChoice && !selectedRegisterId) {
62
+ setSubmitError("Select a register to open");
63
+ return;
64
+ }
65
+ const amount = majorToMinor(parseFloat(openingCash) || 0);
42
66
  try {
43
- await openShift({ openingCash: amount });
44
- dispatch({ type: "OPEN_SHIFT" });
67
+ const openedRegisterId = (await openShift({
68
+ openingCash: amount,
69
+ ...selectedRegisterId ? { registerId: selectedRegisterId } : {}
70
+ }))?.data?.registerId ?? selectedRegisterId ?? null;
71
+ dispatch({
72
+ type: "OPEN_SHIFT",
73
+ registerId: openedRegisterId
74
+ });
45
75
  } catch (err) {
46
- const msg = err.message || "Failed to open register";
47
- setSubmitError(msg);
76
+ const { message } = presentError(err, "The register was not opened. Try again.", { codeMap: SHIFT_OPEN_ERRORS });
77
+ setSubmitError(message);
48
78
  }
49
79
  }, [
50
80
  openingCash,
51
81
  openShift,
52
- dispatch
82
+ dispatch,
83
+ needsRegisterChoice,
84
+ selectedRegisterId
53
85
  ]);
54
- if (isLoadingShift) return /* @__PURE__ */ jsx("div", {
86
+ const openFailure = error ? presentError(error, "The register was not opened. Try again.", { codeMap: SHIFT_OPEN_ERRORS }) : null;
87
+ if (isLoadingShift || isLoadingRegisters) return /* @__PURE__ */ jsx("div", {
55
88
  className: "flex items-center justify-center h-full",
56
89
  children: /* @__PURE__ */ jsx("div", {
57
90
  className: "text-muted-foreground text-sm",
@@ -114,6 +147,29 @@ function ShiftOpenScreen() {
114
147
  })
115
148
  ]
116
149
  }),
150
+ needsRegisterChoice && /* @__PURE__ */ jsxs("div", {
151
+ className: "space-y-2",
152
+ children: [/* @__PURE__ */ jsxs("label", {
153
+ className: "text-sm font-medium flex items-center gap-2",
154
+ children: [/* @__PURE__ */ jsx(Monitor, { className: "h-4 w-4" }), "Select Register"]
155
+ }), /* @__PURE__ */ jsx("div", {
156
+ className: "grid grid-cols-2 gap-2",
157
+ children: activeRegisters.map((r) => /* @__PURE__ */ jsxs("button", {
158
+ type: "button",
159
+ onClick: () => {
160
+ setSelectedRegisterId(r._id);
161
+ },
162
+ className: `rounded-lg border p-3 text-start transition-colors ${selectedRegisterId === r._id ? "border-primary bg-primary/10 ring-1 ring-primary" : "border-border hover:bg-muted"}`,
163
+ children: [/* @__PURE__ */ jsx("div", {
164
+ className: "font-medium text-sm truncate",
165
+ children: r.name
166
+ }), /* @__PURE__ */ jsx("div", {
167
+ className: "text-xs text-muted-foreground",
168
+ children: r.code
169
+ })]
170
+ }, r._id))
171
+ })]
172
+ }),
117
173
  /* @__PURE__ */ jsxs("div", {
118
174
  className: "space-y-2",
119
175
  children: [
@@ -123,15 +179,15 @@ function ShiftOpenScreen() {
123
179
  /* @__PURE__ */ jsx(DollarSign, { className: "h-4 w-4" }),
124
180
  "Starting Cash in Drawer",
125
181
  requiredFloat !== null && /* @__PURE__ */ jsxs("span", {
126
- className: "ml-auto text-xs text-muted-foreground",
127
- children: ["Required: ", formatPaisa(requiredFloat)]
182
+ className: "ms-auto text-xs text-muted-foreground",
183
+ children: ["Required: ", formatMinor(requiredFloat)]
128
184
  })
129
185
  ]
130
186
  }),
131
187
  /* @__PURE__ */ jsx(Numpad, {
132
188
  value: openingCash,
133
189
  onChange: setOpeningCash,
134
- onSubmit: isPending ? void 0 : handleOpenShift,
190
+ onSubmit: isPending || !canOpen ? void 0 : handleOpenShift,
135
191
  submitLabel,
136
192
  quickAdd: [
137
193
  1e3,
@@ -144,13 +200,13 @@ function ShiftOpenScreen() {
144
200
  }),
145
201
  openingCash && !submitError && /* @__PURE__ */ jsxs("p", {
146
202
  className: "text-center text-sm text-muted-foreground",
147
- children: ["Opening with ", formatBdt(parseFloat(openingCash) || 0)]
203
+ children: ["Opening with ", formatMajor(parseFloat(openingCash) || 0)]
148
204
  })
149
205
  ]
150
206
  }),
151
- (submitError || error) && /* @__PURE__ */ jsxs("div", {
207
+ (submitError || openFailure) && /* @__PURE__ */ jsxs("div", {
152
208
  className: "flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200",
153
- children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError || error?.message })]
209
+ children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError ?? openFailure?.message })]
154
210
  }),
155
211
  requiredFloat === null && /* @__PURE__ */ jsx("div", {
156
212
  className: "text-center",
@@ -158,7 +214,7 @@ function ShiftOpenScreen() {
158
214
  variant: "ghost",
159
215
  size: "sm",
160
216
  className: "text-xs text-muted-foreground",
161
- disabled: isPending,
217
+ disabled: isPending || !canOpen,
162
218
  onClick: () => {
163
219
  setOpeningCash("0");
164
220
  handleOpenShift();
@@ -0,0 +1,8 @@
1
+ import { OrderHistoryDrawer } from "./OrderHistoryDrawer.js";
2
+ import { ParkedOrdersDrawer } from "./ParkedOrdersDrawer.js";
3
+ import { PaymentScreen } from "./PaymentScreen.js";
4
+ import { ProductScreen } from "./ProductScreen.js";
5
+ import { ReceiptScreen } from "./ReceiptScreen.js";
6
+ import { ShiftCloseScreen } from "./ShiftCloseScreen.js";
7
+ import { ShiftOpenScreen } from "./ShiftOpenScreen.js";
8
+ export { OrderHistoryDrawer, ParkedOrdersDrawer, PaymentScreen, ProductScreen, ReceiptScreen, ShiftCloseScreen, ShiftOpenScreen };
@@ -0,0 +1,9 @@
1
+ import { ParkedOrdersDrawer } from "./ParkedOrdersDrawer.js";
2
+ import { ProductScreen } from "./ProductScreen.js";
3
+ import { PaymentScreen } from "./PaymentScreen.js";
4
+ import { ReceiptScreen } from "./ReceiptScreen.js";
5
+ import { ShiftOpenScreen } from "./ShiftOpenScreen.js";
6
+ import { ShiftCloseScreen } from "./ShiftCloseScreen.js";
7
+ import { OrderHistoryDrawer } from "./OrderHistoryDrawer.js";
8
+
9
+ export { OrderHistoryDrawer, ParkedOrdersDrawer, PaymentScreen, ProductScreen, ReceiptScreen, ShiftCloseScreen, ShiftOpenScreen };
@@ -1,8 +1,10 @@
1
1
  import { PosHardware } from "../hardware/ports.js";
2
2
  import { createWebHardware } from "../hardware/web-adapter.js";
3
3
  import { createTauriHardware } from "../hardware/tauri-adapter.js";
4
+ import "../hardware/index.js";
4
5
  import { PosAuthPort, PosSessionUser } from "../runtime/auth-port.js";
5
6
  import { PosRuntime } from "../runtime/config.js";
6
7
  import { PosBranch, PosBranchPort } from "../runtime/branch-port.js";
8
+ import "../runtime/index.js";
7
9
  import { PosShell, PosShellProps } from "./pos-shell.js";
8
10
  export { type PosAuthPort, type PosBranch, type PosBranchPort, type PosHardware, type PosRuntime, type PosSessionUser, PosShell, type PosShellProps, createTauriHardware, createWebHardware };