@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,132 @@
1
+ "use client";
2
+
3
+ import { PosAuthProvider, usePosAuth } from "../runtime/auth-port.js";
4
+ import { PosRuntimeProvider } from "../runtime/config.js";
5
+ import { PosBranchProvider } from "../runtime/branch-port.js";
6
+ import { createWebHardware } from "../hardware/web-adapter.js";
7
+ import { HardwareProvider } from "../hardware/context.js";
8
+ import { PosProvider, usePosContext } from "../state/pos-context.js";
9
+ import { PosTopBar } from "../components/PosTopBar.js";
10
+ import { ProductScreen } from "../screens/ProductScreen.js";
11
+ import { PaymentScreen } from "../screens/PaymentScreen.js";
12
+ import { ReceiptScreen } from "../screens/ReceiptScreen.js";
13
+ import { ShiftOpenScreen } from "../screens/ShiftOpenScreen.js";
14
+ import { ShiftCloseScreen } from "../screens/ShiftCloseScreen.js";
15
+ import { OrderHistoryDrawer } from "../screens/OrderHistoryDrawer.js";
16
+ import { PrinterSettingsDialog } from "../components/PrinterSettingsDialog.js";
17
+ import { useCallback, useEffect, useMemo, useState } from "react";
18
+ import { useKeyboardShortcut, useWakeLock } from "@classytic/fluid/client/hooks";
19
+ import { DensityProvider, HotkeyHelpDialog, HotkeyRegistryProvider } from "@classytic/fluid/client/core";
20
+ import { usePosCurrentShift } from "@classytic/commerce-sdk/sales";
21
+ import { jsx, jsxs } from "react/jsx-runtime";
22
+
23
+ //#region src/shell/pos-shell.tsx
24
+ /**
25
+ * PosShell — Top-level POS orchestrator.
26
+ *
27
+ * Renders the active screen based on the state machine.
28
+ * Mounts all DI providers (auth / branch / runtime / hardware) so a host
29
+ * needs a single import to stand up the register. Wraps everything in
30
+ * PosProvider for shared context.
31
+ *
32
+ * Screen flow: idle (shift open) → products → payment → receipt → products
33
+ */
34
+ function PosScreenRouter() {
35
+ const { state, dispatch } = usePosContext();
36
+ const sessionUser = usePosAuth().useSessionUser();
37
+ const { shift: activeShift } = usePosCurrentShift();
38
+ const userName = sessionUser?.name || "Cashier";
39
+ const [historyOpen, setHistoryOpen] = useState(false);
40
+ const [helpOpen, setHelpOpen] = useState(false);
41
+ const [printerSettingsOpen, setPrinterSettingsOpen] = useState(false);
42
+ useWakeLock({ enabled: state.shiftOpen });
43
+ useEffect(() => {
44
+ if (state.screen === "idle" && activeShift?.state === "open" && !state.shiftOpen) dispatch({ type: "OPEN_SHIFT" });
45
+ }, [
46
+ state.screen,
47
+ state.shiftOpen,
48
+ activeShift,
49
+ dispatch
50
+ ]);
51
+ useKeyboardShortcut("f2", () => {
52
+ if (state.screen === "receipt") dispatch({ type: "NEW_SALE" });
53
+ }, {
54
+ description: "New sale (after receipt)",
55
+ category: "Sale"
56
+ });
57
+ useKeyboardShortcut("escape", () => {
58
+ dispatch({ type: "GO_BACK" });
59
+ }, {
60
+ disableInInputs: false,
61
+ description: "Back / cancel current step",
62
+ category: "Navigation"
63
+ });
64
+ useKeyboardShortcut("f9", () => {
65
+ if (state.shiftOpen) setHistoryOpen(true);
66
+ }, {
67
+ description: "Open order history",
68
+ category: "Navigation"
69
+ });
70
+ useKeyboardShortcut("shift+/", () => setHelpOpen(true), {
71
+ description: "Show keyboard shortcuts",
72
+ category: "Help"
73
+ });
74
+ const handleNewSale = useCallback(() => {
75
+ if (state.screen === "receipt") dispatch({ type: "NEW_SALE" });
76
+ }, [state.screen, dispatch]);
77
+ return /* @__PURE__ */ jsxs("div", {
78
+ className: "flex flex-col h-screen",
79
+ children: [
80
+ state.screen !== "idle" && /* @__PURE__ */ jsx(PosTopBar, {
81
+ userName,
82
+ onNewSale: state.screen === "products" ? handleNewSale : void 0,
83
+ onOpenHistory: () => setHistoryOpen(true),
84
+ onOpenHelp: () => setHelpOpen(true),
85
+ onOpenPrinterSettings: () => setPrinterSettingsOpen(true)
86
+ }),
87
+ /* @__PURE__ */ jsxs("div", {
88
+ className: "flex-1 min-h-0",
89
+ children: [
90
+ state.screen === "idle" && /* @__PURE__ */ jsx(ShiftOpenScreen, {}),
91
+ state.screen === "products" && /* @__PURE__ */ jsx(ProductScreen, {}),
92
+ state.screen === "payment" && /* @__PURE__ */ jsx(PaymentScreen, {}),
93
+ state.screen === "receipt" && /* @__PURE__ */ jsx(ReceiptScreen, {}),
94
+ state.screen === "closing" && /* @__PURE__ */ jsx(ShiftCloseScreen, {})
95
+ ]
96
+ }),
97
+ /* @__PURE__ */ jsx(OrderHistoryDrawer, {
98
+ open: historyOpen,
99
+ onOpenChange: setHistoryOpen
100
+ }),
101
+ /* @__PURE__ */ jsx(HotkeyHelpDialog, {
102
+ open: helpOpen,
103
+ onOpenChange: setHelpOpen
104
+ }),
105
+ /* @__PURE__ */ jsx(PrinterSettingsDialog, {
106
+ open: printerSettingsOpen,
107
+ onOpenChange: setPrinterSettingsOpen
108
+ })
109
+ ]
110
+ });
111
+ }
112
+ function PosShell({ auth, branch, runtime, hardware }) {
113
+ return /* @__PURE__ */ jsx(PosAuthProvider, {
114
+ auth,
115
+ children: /* @__PURE__ */ jsx(PosBranchProvider, {
116
+ branch,
117
+ children: /* @__PURE__ */ jsx(PosRuntimeProvider, {
118
+ runtime,
119
+ children: /* @__PURE__ */ jsx(DensityProvider, {
120
+ density: "auto",
121
+ children: /* @__PURE__ */ jsx(HotkeyRegistryProvider, { children: /* @__PURE__ */ jsx(HardwareProvider, {
122
+ hardware: useMemo(() => hardware ?? createWebHardware(), [hardware]),
123
+ children: /* @__PURE__ */ jsx(PosProvider, { children: /* @__PURE__ */ jsx(PosScreenRouter, {}) })
124
+ }) })
125
+ })
126
+ })
127
+ })
128
+ });
129
+ }
130
+
131
+ //#endregion
132
+ export { PosShell };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { INITIAL_POS_STATE, posReducer } from "./pos-state.js";
4
+ import { createContext, useContext, useReducer } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+
7
+ //#region src/state/pos-context.tsx
8
+ /**
9
+ * POS Context — Provides screen state, dispatch, and session info
10
+ * to all POS screens without prop drilling.
11
+ *
12
+ * Wrap PosShell with <PosProvider> so any screen can:
13
+ * const { screen, dispatch } = usePosContext();
14
+ */
15
+ const PosContext = createContext(null);
16
+ function PosProvider({ children }) {
17
+ const [state, dispatch] = useReducer(posReducer, INITIAL_POS_STATE);
18
+ return /* @__PURE__ */ jsx(PosContext, {
19
+ value: {
20
+ state,
21
+ dispatch
22
+ },
23
+ children
24
+ });
25
+ }
26
+ function usePosContext() {
27
+ const ctx = useContext(PosContext);
28
+ if (!ctx) throw new Error("usePosContext must be used within <PosProvider>");
29
+ return ctx;
30
+ }
31
+
32
+ //#endregion
33
+ export { PosProvider, usePosContext };
@@ -0,0 +1,70 @@
1
+ //#region src/state/pos-state.ts
2
+ const INITIAL_POS_STATE = {
3
+ screen: "idle",
4
+ lastReceipt: null,
5
+ lastCashReceived: null,
6
+ shiftOpen: false
7
+ };
8
+ function posReducer(state, action) {
9
+ switch (action.type) {
10
+ case "OPEN_SHIFT":
11
+ if (state.screen !== "idle") return state;
12
+ return {
13
+ ...state,
14
+ screen: "products",
15
+ shiftOpen: true
16
+ };
17
+ case "GO_TO_PAYMENT":
18
+ if (state.screen !== "products") return state;
19
+ return {
20
+ ...state,
21
+ screen: "payment"
22
+ };
23
+ case "GO_BACK":
24
+ if (state.screen === "payment") return {
25
+ ...state,
26
+ screen: "products"
27
+ };
28
+ if (state.screen === "receipt") return {
29
+ ...state,
30
+ screen: "products",
31
+ lastReceipt: null,
32
+ lastCashReceived: null
33
+ };
34
+ return state;
35
+ case "COMPLETE_SALE":
36
+ if (state.screen !== "payment") return state;
37
+ return {
38
+ ...state,
39
+ screen: "receipt",
40
+ lastReceipt: action.receipt
41
+ };
42
+ case "NEW_SALE":
43
+ if (state.screen !== "receipt") return state;
44
+ return {
45
+ ...state,
46
+ screen: "products",
47
+ lastReceipt: null,
48
+ lastCashReceived: null
49
+ };
50
+ case "CLOSE_SHIFT":
51
+ if (!state.shiftOpen) return state;
52
+ return {
53
+ ...state,
54
+ screen: "closing"
55
+ };
56
+ case "CONFIRM_CLOSE":
57
+ if (state.screen !== "closing") return state;
58
+ return { ...INITIAL_POS_STATE };
59
+ case "CANCEL_CLOSE":
60
+ if (state.screen !== "closing") return state;
61
+ return {
62
+ ...state,
63
+ screen: "products"
64
+ };
65
+ default: return state;
66
+ }
67
+ }
68
+
69
+ //#endregion
70
+ export { INITIAL_POS_STATE, posReducer };
@@ -0,0 +1,35 @@
1
+ //#region src/utils/customer-display.ts
2
+ /**
3
+ * Best display name for a customer card. Falls back through:
4
+ * 1. Backend virtual `fullName`
5
+ * 2. Backend virtual `displayName`
6
+ * 3. Composed from structured `name.given` + `name.family`
7
+ * 4. Email local-part (before @)
8
+ * 5. Literal "Customer"
9
+ */
10
+ function customerDisplayName(c) {
11
+ if (!c) return "Customer";
12
+ const v = (c.fullName ?? c.displayName ?? "").trim();
13
+ if (v) return v;
14
+ const composed = `${(c.name?.given ?? "").trim()} ${(c.name?.family ?? "").trim()}`.trim();
15
+ if (composed) return composed;
16
+ const email = c.contact?.email ?? "";
17
+ const local = email.includes("@") ? email.split("@")[0]?.trim() : "";
18
+ if (local) return local;
19
+ return "Customer";
20
+ }
21
+ /**
22
+ * Returns the customer's phone for display, or `null` if the value is
23
+ * a server-side placeholder like `pending_<userId>` (auto-generated when
24
+ * a user record exists without a real phone). Pre-release records used
25
+ * `pending_` prefix to satisfy the unique index without surfacing junk.
26
+ */
27
+ function customerDisplayPhone(c) {
28
+ const raw = c?.contact?.phone?.trim();
29
+ if (!raw) return null;
30
+ if (raw.startsWith("pending_")) return null;
31
+ return raw;
32
+ }
33
+
34
+ //#endregion
35
+ export { customerDisplayName, customerDisplayPhone };
@@ -0,0 +1,242 @@
1
+ import { Paisa } from "../lib/money.js";
2
+ import { isValidPhone } from "@classytic/commerce-sdk/client";
3
+
4
+ //#region src/utils/pos-helpers.ts
5
+ /** Brand a paisa value read out of an unknown response. The number is
6
+ * expected to already be in paisa (be-prod canonical unit); this helper
7
+ * just re-asserts the brand after a `typeof` guard. */
8
+ const paisaOr0 = (v) => Paisa(typeof v === "number" ? v : 0);
9
+ /**
10
+ * Parse cash received input to number
11
+ */
12
+ function parseCashReceived(value) {
13
+ const n = Number(value);
14
+ return Number.isFinite(n) ? n : 0;
15
+ }
16
+ /**
17
+ * Parse input to positive number (min 0)
18
+ */
19
+ function parsePositiveNumber(value) {
20
+ const n = Number(value);
21
+ if (!Number.isFinite(n)) return 0;
22
+ return n > 0 ? n : 0;
23
+ }
24
+ /**
25
+ * Calculate cart totals
26
+ */
27
+ function calculateCartTotals(items, discountInput) {
28
+ const subtotal = items.reduce((sum, item) => sum + item.lineTotal, 0);
29
+ const rawDiscount = parsePositiveNumber(discountInput);
30
+ const discount = subtotal <= 0 ? 0 : Math.min(rawDiscount, subtotal);
31
+ return {
32
+ subtotal,
33
+ discount,
34
+ total: Math.max(0, subtotal - discount),
35
+ itemCount: items.length
36
+ };
37
+ }
38
+ function validateRedemption(pointsToRedeem, customerPoints, orderTotal, membershipConfig) {
39
+ if (!membershipConfig?.enabled) return {
40
+ valid: false,
41
+ error: "Membership program disabled"
42
+ };
43
+ const config = membershipConfig?.redemption;
44
+ if (!config?.enabled) return {
45
+ valid: false,
46
+ error: "Points redemption not enabled"
47
+ };
48
+ if (pointsToRedeem < (config.minRedeemPoints ?? 0)) return {
49
+ valid: false,
50
+ error: `Minimum ${config.minRedeemPoints} points required`
51
+ };
52
+ if (pointsToRedeem > customerPoints) return {
53
+ valid: false,
54
+ error: `Insufficient points. Available: ${customerPoints}`
55
+ };
56
+ if (orderTotal < (config.minOrderAmount ?? 0)) return {
57
+ valid: false,
58
+ error: `Minimum order of ৳${config.minOrderAmount} required`
59
+ };
60
+ const maxDiscount = Math.floor(orderTotal * (config.maxRedeemPercent ?? 100) / 100);
61
+ const requestedDiscount = Math.floor(pointsToRedeem / (config.pointsPerBdt ?? 1));
62
+ let actualDiscount = requestedDiscount;
63
+ let actualPoints = pointsToRedeem;
64
+ if (requestedDiscount > maxDiscount) {
65
+ actualDiscount = maxDiscount;
66
+ actualPoints = maxDiscount * (config.pointsPerBdt ?? 1);
67
+ }
68
+ return {
69
+ valid: true,
70
+ discountAmount: actualDiscount,
71
+ pointsToRedeem: actualPoints,
72
+ maxAllowedPoints: maxDiscount * (config.pointsPerBdt ?? 1)
73
+ };
74
+ }
75
+ function calculateOrderTotals({ items, manualDiscountInput, membershipConfig, customer, pointsToRedeemInput }) {
76
+ const subtotal = items.reduce((sum, item) => sum + item.lineTotal, 0);
77
+ const manualDiscount = Math.min(parsePositiveNumber(manualDiscountInput), subtotal);
78
+ const tierName = customer?.membership?.tier || null;
79
+ const customerPoints = customer?.membership?.points?.current || 0;
80
+ const tierDiscount = 0;
81
+ const preliminaryTotal = Math.max(0, subtotal - manualDiscount);
82
+ const requestedPoints = Math.floor(parsePositiveNumber(pointsToRedeemInput));
83
+ let redemptionDiscount = 0;
84
+ let actualPointsRedeemed = 0;
85
+ let redemptionError = null;
86
+ let maxAllowedPoints = 0;
87
+ if (requestedPoints > 0) {
88
+ const result = validateRedemption(requestedPoints, customerPoints, preliminaryTotal, membershipConfig);
89
+ if (result.valid) {
90
+ redemptionDiscount = result.discountAmount ?? 0;
91
+ actualPointsRedeemed = result.pointsToRedeem ?? 0;
92
+ maxAllowedPoints = result.maxAllowedPoints ?? 0;
93
+ } else redemptionError = result.error ?? "Redemption not available";
94
+ }
95
+ if (!maxAllowedPoints && membershipConfig?.enabled && membershipConfig?.redemption?.enabled) maxAllowedPoints = Math.floor(preliminaryTotal * (membershipConfig.redemption.maxRedeemPercent ?? 100) / 100) * (membershipConfig.redemption.pointsPerBdt ?? 1);
96
+ const total = Math.max(0, preliminaryTotal - redemptionDiscount);
97
+ return {
98
+ subtotal,
99
+ manualDiscount,
100
+ tierDiscount,
101
+ redemptionDiscount,
102
+ totalDiscount: manualDiscount + tierDiscount + redemptionDiscount,
103
+ total,
104
+ pointsToEarn: 0,
105
+ pointsRedeemed: actualPointsRedeemed,
106
+ redemptionError,
107
+ maxAllowedPoints,
108
+ tierName
109
+ };
110
+ }
111
+ /**
112
+ * Calculate change amount for cash payment
113
+ */
114
+ function calculateChange(cashReceived, total) {
115
+ return Math.max(0, cashReceived - total);
116
+ }
117
+ /**
118
+ * Calculate amount due for cash payment
119
+ */
120
+ function calculateAmountDue(cashReceived, total) {
121
+ return Math.max(0, total - cashReceived);
122
+ }
123
+ /**
124
+ * Validate a customer phone against the deployment's configured pattern
125
+ * (country-agnostic — `configureSDK({ receipt: { phonePattern } })`; defaults
126
+ * to Bangladesh). Kept the `BD` name for call-site compatibility; the check is
127
+ * now locale-driven, not hardcoded.
128
+ */
129
+ function isValidBDPhone(phone) {
130
+ return isValidPhone(phone);
131
+ }
132
+ /**
133
+ * Check if a search string looks like a phone number. `exact` uses the
134
+ * deployment's phone pattern (locale-driven); `likely` is a loose digit-run
135
+ * heuristic that stays country-neutral.
136
+ */
137
+ function isPhoneSearch(query) {
138
+ const trimmed = query.trim();
139
+ return {
140
+ exact: isValidPhone(trimmed),
141
+ likely: /^\d{4,}$/.test(trimmed)
142
+ };
143
+ }
144
+ /** Read paisa from a Money struct `{ amount, currency }` returned by the
145
+ * order kernel. Falls back to a bare number for legacy shapes. */
146
+ const moneyAmount = (m) => {
147
+ if (m && typeof m === "object" && "amount" in m && typeof m.amount === "number") return Paisa(m.amount);
148
+ return paisaOr0(m);
149
+ };
150
+ /**
151
+ * Transform a POS order response to receipt format.
152
+ *
153
+ * The order kernel (`@classytic/order`) returns:
154
+ * - `order.totals.{ subtotal, discount, tax, shipping, grandTotal }` — each is `Money = { amount, currency }`
155
+ * - `order.lines[]` — each line has `quantity`, `snapshot.{ sku, name, variantLabel }`,
156
+ * `unitPrice/lineTotal/unitTax: Money`
157
+ * - `order.customerSnapshot.{ name, phone? }` (no email for walk-in)
158
+ * - `order.paymentState.totalCharged: Money` (after capture)
159
+ * - POS-specific fields (membership, VAT) live in `order.metadata.pos`
160
+ *
161
+ * The wire format inside Money structs is integer paisa for BDT.
162
+ */
163
+ /**
164
+ * The order's server-authored timestamp for the receipt. Never falls back to
165
+ * the register's clock — a fabricated time on a fiscal document is worse
166
+ * than a visibly blank one. Missing `createdAt` is logged as data corruption.
167
+ */
168
+ function resolveOrderDate(order) {
169
+ const raw = order.createdAt || order.placedAt;
170
+ if (!raw) {
171
+ console.error("[pos-ui] order missing createdAt/placedAt — receipt date left blank", order._id ?? order.orderNumber);
172
+ return "";
173
+ }
174
+ return String(raw);
175
+ }
176
+ function transformOrderToReceipt(orderResponse, branchInfo) {
177
+ if (!orderResponse || typeof orderResponse !== "object") return null;
178
+ const response = orderResponse;
179
+ const order = response.data ?? response;
180
+ if (!order._id && !order.orderNumber) return null;
181
+ const totals = order.totals ?? {};
182
+ const customerSnap = order.customerSnapshot ?? {};
183
+ const lines = order.lines ?? [];
184
+ const paymentState = order.paymentState ?? {};
185
+ const meta = order.metadata ?? {};
186
+ const membership = meta.membership ?? {};
187
+ const vat = meta.vat ?? {};
188
+ const primaryPayment = (meta.payments ?? [])[0];
189
+ const itemVatAmount = (line) => {
190
+ return Paisa(Number(moneyAmount(line.unitTax)) * (typeof line.quantity === "number" ? line.quantity : 1));
191
+ };
192
+ return {
193
+ orderId: String(order._id || ""),
194
+ orderNumber: String(order.orderNumber || order._id || "").slice(-8).toUpperCase(),
195
+ invoiceNumber: order.invoiceNumber ? String(order.invoiceNumber) : void 0,
196
+ date: resolveOrderDate(order),
197
+ branch: branchInfo,
198
+ cashier: order.cashierName ? String(order.cashierName) : void 0,
199
+ customer: {
200
+ name: customerSnap.name ? String(customerSnap.name) : void 0,
201
+ phone: customerSnap.phone ? String(customerSnap.phone) : null
202
+ },
203
+ membership: membership.cardId ? {
204
+ cardId: String(membership.cardId),
205
+ tier: membership.tier ? String(membership.tier) : void 0,
206
+ pointsEarned: typeof membership.pointsEarned === "number" ? membership.pointsEarned : void 0,
207
+ tierDiscount: paisaOr0(membership.tierDiscountApplied)
208
+ } : void 0,
209
+ items: lines.map((line) => {
210
+ const snap = line.snapshot ?? {};
211
+ return {
212
+ name: String(snap.name || line.productName || "Item"),
213
+ variant: snap.variantLabel ? String(snap.variantLabel) : snap.sku ? String(snap.sku) : void 0,
214
+ quantity: typeof line.quantity === "number" ? line.quantity : 1,
215
+ unitPrice: moneyAmount(line.unitPrice),
216
+ total: moneyAmount(line.lineTotal),
217
+ vatRate: typeof line.taxRate === "number" ? line.taxRate : 0,
218
+ vatAmount: itemVatAmount(line)
219
+ };
220
+ }),
221
+ subtotal: moneyAmount(totals.subtotal),
222
+ discount: moneyAmount(totals.discount),
223
+ deliveryCharge: moneyAmount(totals.shipping),
224
+ vat: {
225
+ applicable: !!vat.applicable,
226
+ rate: typeof vat.rate === "number" ? vat.rate : 0,
227
+ amount: moneyAmount(totals.tax),
228
+ taxableAmount: paisaOr0(vat.taxableAmount),
229
+ sellerBin: vat.sellerBin ? String(vat.sellerBin) : void 0,
230
+ pricesIncludeVat: typeof vat.pricesIncludeVat === "boolean" ? vat.pricesIncludeVat : true
231
+ },
232
+ total: moneyAmount(totals.grandTotal),
233
+ payment: {
234
+ method: primaryPayment?.method ?? "cash",
235
+ ...primaryPayment?.reference ? { reference: primaryPayment.reference } : {},
236
+ amount: moneyAmount(paymentState.totalCharged) || moneyAmount(totals.grandTotal)
237
+ }
238
+ };
239
+ }
240
+
241
+ //#endregion
242
+ export { calculateAmountDue, calculateCartTotals, calculateChange, calculateOrderTotals, isPhoneSearch, isValidBDPhone, parseCashReceived, parsePositiveNumber, transformOrderToReceipt, validateRedemption };
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@classytic/pos-ui",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Point-of-sale register UI — a self-contained, full-screen state-machine app (shift → products → payment → receipt → close) with a hardware DI port (printer/drawer/scanner/terminal). Multi-surface: web, Tauri desktop, React Native. Frontend sibling of the @classytic/pos kernel. Independent of the dashboard stack (no erp-shell).",
7
+ "files": [
8
+ "dist",
9
+ "styles.css",
10
+ "LICENSE",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsdown",
15
+ "dev": "tsdown --watch",
16
+ "typecheck": "tsc --noEmit",
17
+ "clean": "rimraf dist"
18
+ },
19
+ "peerDependencies": {
20
+ "@classytic/commerce-sdk": ">=0.9.0",
21
+ "@classytic/fluid": ">=1.7.0",
22
+ "@classytic/primitives": ">=0.9.0",
23
+ "@tanstack/react-query": ">=5.0.0",
24
+ "clsx": ">=2.0.0",
25
+ "lucide-react": ">=1.0.0",
26
+ "next": ">=14.0.0",
27
+ "react": ">=19.0.0",
28
+ "sonner": ">=2.0.0",
29
+ "tailwind-merge": ">=2.0.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "@classytic/commerce-sdk": {
33
+ "optional": true
34
+ },
35
+ "@classytic/fluid": {
36
+ "optional": true
37
+ },
38
+ "@classytic/primitives": {
39
+ "optional": true
40
+ },
41
+ "@tanstack/react-query": {
42
+ "optional": true
43
+ },
44
+ "clsx": {
45
+ "optional": true
46
+ },
47
+ "lucide-react": {
48
+ "optional": true
49
+ },
50
+ "next": {
51
+ "optional": true
52
+ },
53
+ "react": {
54
+ "optional": true
55
+ },
56
+ "sonner": {
57
+ "optional": true
58
+ },
59
+ "tailwind-merge": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "devDependencies": {
64
+ "@tanstack/react-query": "^5.101.2",
65
+ "@types/node": "^26.1.0",
66
+ "@types/react": "^19.2.17",
67
+ "clsx": "^2.1.1",
68
+ "lucide-react": "^1.23.0",
69
+ "next": "^16.2.9",
70
+ "publint": "^0.3.21",
71
+ "react": "^19.2.7",
72
+ "react-hook-form": "^7.80.0",
73
+ "rimraf": "^6.1.3",
74
+ "sonner": "^2.0.7",
75
+ "tailwind-merge": "^3.6.0",
76
+ "tsdown": "^0.22.3",
77
+ "typescript": "^6.0.3"
78
+ },
79
+ "publishConfig": {
80
+ "access": "public"
81
+ },
82
+ "sideEffects": false,
83
+ "exports": {
84
+ "./hardware": "./dist/hardware/index.js",
85
+ "./runtime": "./dist/runtime/index.js",
86
+ "./shell": "./dist/shell/index.js",
87
+ "./package.json": "./package.json"
88
+ },
89
+ "inlinedDependencies": {
90
+ "react-hook-form": "7.80.0"
91
+ }
92
+ }
package/styles.css ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @classytic/pos-ui - Tailwind CSS Integration
3
+ *
4
+ * Import this file in your app's global CSS so Tailwind discovers the
5
+ * utility classes used inside the compiled POS components. Without it,
6
+ * classes that appear ONLY in pos-ui (e.g. the `flex-[65]` / `flex-[35]`
7
+ * product/cart split, `flex-[50]` payment columns) are never generated in
8
+ * the host's CSS bundle and the register layout collapses.
9
+ *
10
+ * Mirrors the fluid convention (`@classytic/fluid/styles.css`).
11
+ *
12
+ * Usage (in app/globals.css or similar, AFTER `@import "tailwindcss"`):
13
+ * @import "@classytic/pos-ui/styles.css";
14
+ */
15
+
16
+ /* Scan the compiled JS for Tailwind classes. pos-ui builds to `.js`
17
+ * (see package.json exports), so match that — not `.mjs`. Type defs are
18
+ * skipped by the extension filter. */
19
+ @source "./dist/**/*.js";