@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,194 @@
1
+ "use client";
2
+
3
+ import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { calculateAmountDue, calculateChange, parseCashReceived, parsePositiveNumber } from "../utils/pos-helpers.js";
5
+ import { useCallback, useEffect, useMemo, useState } from "react";
6
+ import { usePaymentMethods } from "@classytic/commerce-sdk/platform";
7
+
8
+ //#region src/hooks/usePosPayment.ts
9
+ function getPaymentKey(method, index) {
10
+ return method._id || `${method.type}:${method.provider || ""}:${method.name}:${index}`;
11
+ }
12
+ function mapPlatformMethodToPosMethod(method) {
13
+ switch (method.type) {
14
+ case "cash": return "cash";
15
+ case "mfs": {
16
+ const provider = (method.provider || "").toLowerCase();
17
+ if (provider === "bkash" || provider === "nagad" || provider === "rocket" || provider === "upay") return provider;
18
+ return null;
19
+ }
20
+ case "bank_transfer": return "bank_transfer";
21
+ case "card": return "card";
22
+ default: return null;
23
+ }
24
+ }
25
+ function paymentNeedsReference(posMethod) {
26
+ return posMethod !== "cash";
27
+ }
28
+ function createSplitEntry(option) {
29
+ const suffix = Math.random().toString(16).slice(2);
30
+ return {
31
+ id: `split_${Date.now()}_${suffix}`,
32
+ paymentKey: option.key,
33
+ posMethod: option.posMethod,
34
+ amount: "",
35
+ reference: "",
36
+ error: void 0
37
+ };
38
+ }
39
+ function usePosPayment(token, total = 0) {
40
+ usePosAuth().getToken();
41
+ const [selectedKey, setSelectedKey] = useState(null);
42
+ const [cashReceived, setCashReceived] = useState("");
43
+ const [reference, setReference] = useState("");
44
+ const [mode, setMode] = useState("single");
45
+ const [splitEntries, setSplitEntries] = useState([]);
46
+ const { paymentMethods: platformMethods, isLoading } = usePaymentMethods();
47
+ const options = useMemo(() => {
48
+ return (platformMethods || []).filter((m) => m?.isActive !== false).map((m, idx) => {
49
+ const posMethod = mapPlatformMethodToPosMethod(m);
50
+ if (!posMethod) return null;
51
+ return {
52
+ key: getPaymentKey(m, idx),
53
+ posMethod,
54
+ label: m.name,
55
+ needsReference: paymentNeedsReference(posMethod),
56
+ note: m.note,
57
+ walletNumber: m.type === "mfs" ? m.walletNumber : void 0
58
+ };
59
+ }).filter(Boolean);
60
+ }, [platformMethods]);
61
+ useEffect(() => {
62
+ if (selectedKey || options.length === 0) return;
63
+ const cash = options.find((o) => o.posMethod === "cash");
64
+ setSelectedKey((cash || options[0])?.key ?? null);
65
+ }, [options, selectedKey]);
66
+ const selectedOption = useMemo(() => {
67
+ if (options.length === 0) return null;
68
+ return options.find((o) => o.key === selectedKey) || options[0];
69
+ }, [options, selectedKey]);
70
+ const selectedMethod = selectedOption?.posMethod || "cash";
71
+ useEffect(() => {
72
+ if (mode !== "single") return;
73
+ setReference("");
74
+ setCashReceived("");
75
+ }, [selectedMethod, mode]);
76
+ useEffect(() => {
77
+ if (mode !== "split" || splitEntries.length > 0) return;
78
+ const opt = selectedOption || options[0];
79
+ if (opt) setSplitEntries([createSplitEntry(opt)]);
80
+ }, [
81
+ mode,
82
+ splitEntries.length,
83
+ selectedOption,
84
+ options
85
+ ]);
86
+ const cashReceivedNumber = useMemo(() => parseCashReceived(cashReceived), [cashReceived]);
87
+ const changeAmount = useMemo(() => {
88
+ if (selectedMethod !== "cash") return 0;
89
+ return calculateChange(cashReceivedNumber, total);
90
+ }, [
91
+ cashReceivedNumber,
92
+ total,
93
+ selectedMethod
94
+ ]);
95
+ const amountDue = useMemo(() => {
96
+ if (selectedMethod !== "cash") return 0;
97
+ return calculateAmountDue(cashReceivedNumber, total);
98
+ }, [
99
+ cashReceivedNumber,
100
+ total,
101
+ selectedMethod
102
+ ]);
103
+ const splitTotal = useMemo(() => splitEntries.reduce((sum, e) => sum + parsePositiveNumber(e.amount), 0), [splitEntries]);
104
+ const splitRemaining = total - splitTotal;
105
+ const splitIsBalanced = Math.abs(splitRemaining) < .01 && splitTotal > 0;
106
+ const validateSplitEntry = useCallback((id) => {
107
+ const entry = splitEntries.find((e) => e.id === id);
108
+ if (!entry) return void 0;
109
+ const opt = options.find((o) => o.key === entry.paymentKey);
110
+ if (!opt) return "Invalid payment method";
111
+ if (parsePositiveNumber(entry.amount) <= 0) return "Amount required";
112
+ if (opt.needsReference && !entry.reference.trim()) return `Reference required for ${opt.label}`;
113
+ }, [splitEntries, options]);
114
+ const validateAllSplits = useCallback(() => {
115
+ let hasError = false;
116
+ setSplitEntries((prev) => prev.map((entry) => {
117
+ const opt = options.find((o) => o.key === entry.paymentKey);
118
+ let error;
119
+ if (!opt) error = "Invalid method";
120
+ else if (parsePositiveNumber(entry.amount) <= 0) error = "Amount required";
121
+ else if (opt.needsReference && !entry.reference.trim()) error = "Reference required";
122
+ if (error) hasError = true;
123
+ return {
124
+ ...entry,
125
+ error
126
+ };
127
+ }));
128
+ return !hasError;
129
+ }, [options]);
130
+ const selectPayment = useCallback((key) => setSelectedKey(key), []);
131
+ const addSplit = useCallback((paymentKey) => {
132
+ const opt = paymentKey ? options.find((o) => o.key === paymentKey) : selectedOption || options[0];
133
+ if (!opt) return;
134
+ setSplitEntries((prev) => [...prev, createSplitEntry(opt)]);
135
+ }, [options, selectedOption]);
136
+ const updateSplit = useCallback((id, patch) => {
137
+ setSplitEntries((prev) => prev.map((entry) => {
138
+ if (entry.id !== id) return entry;
139
+ const updated = {
140
+ ...entry,
141
+ ...patch,
142
+ error: void 0
143
+ };
144
+ if (patch.paymentKey) {
145
+ const opt = options.find((o) => o.key === patch.paymentKey);
146
+ if (opt) {
147
+ updated.posMethod = opt.posMethod;
148
+ if (patch.paymentKey !== entry.paymentKey) updated.reference = "";
149
+ }
150
+ }
151
+ return updated;
152
+ }));
153
+ }, [options]);
154
+ const removeSplit = useCallback((id) => {
155
+ setSplitEntries((prev) => prev.filter((e) => e.id !== id));
156
+ }, []);
157
+ const reset = useCallback(() => {
158
+ setReference("");
159
+ setCashReceived("");
160
+ setMode("single");
161
+ setSplitEntries([]);
162
+ }, []);
163
+ return {
164
+ options,
165
+ isLoading,
166
+ state: {
167
+ mode,
168
+ selectedKey,
169
+ selectedMethod,
170
+ reference,
171
+ cashReceived,
172
+ changeAmount,
173
+ amountDue,
174
+ splitEntries,
175
+ splitTotal,
176
+ splitRemaining,
177
+ splitIsBalanced
178
+ },
179
+ selectedOption,
180
+ selectPayment,
181
+ setMode,
182
+ setCashReceived,
183
+ setReference,
184
+ addSplit,
185
+ updateSplit,
186
+ removeSplit,
187
+ validateSplitEntry,
188
+ validateAllSplits,
189
+ reset
190
+ };
191
+ }
192
+
193
+ //#endregion
194
+ export { paymentNeedsReference, usePosPayment };
package/dist/lib/cn.js ADDED
@@ -0,0 +1,12 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ //#region src/lib/cn.ts
5
+ /** Tailwind-aware className merge — same `cn` contract as `@classytic/fluid` and
6
+ * host shadcn, so components compose predictably. */
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+
11
+ //#endregion
12
+ export { cn };
@@ -0,0 +1,30 @@
1
+ //#region src/lib/loyalty.ts
2
+ /**
3
+ * Loyalty display helpers — pure functions for tier colors and readable
4
+ * contrast text. Copied verbatim from the host (no `@/` deps) so the POS
5
+ * package stays self-contained.
6
+ */
7
+ const DEFAULT_TIER_COLORS = {
8
+ bronze: "#CD7F32",
9
+ silver: "#C0C0C0",
10
+ gold: "#FFD700",
11
+ platinum: "#E5E4E2"
12
+ };
13
+ function getTierColor(tierName) {
14
+ if (!tierName) return null;
15
+ const normalized = tierName.toLowerCase();
16
+ return DEFAULT_TIER_COLORS[normalized] || null;
17
+ }
18
+ function getReadableTextColor(background) {
19
+ if (!background) return "#111827";
20
+ if (!background.startsWith("#")) return "#111827";
21
+ const hex = background.replace("#", "");
22
+ if (hex.length !== 6) return "#111827";
23
+ const r = parseInt(hex.slice(0, 2), 16);
24
+ const g = parseInt(hex.slice(2, 4), 16);
25
+ const b = parseInt(hex.slice(4, 6), 16);
26
+ return (.299 * r + .587 * g + .114 * b) / 255 > .6 ? "#111827" : "#FFFFFF";
27
+ }
28
+
29
+ //#endregion
30
+ export { getReadableTextColor, getTierColor };
@@ -0,0 +1,41 @@
1
+ import { MinorUnits, money, toMajor } from "@classytic/primitives/money";
2
+
3
+ //#region src/lib/money.ts
4
+ /**
5
+ * Money for POS — composed on `@classytic/primitives` (the canonical Money type
6
+ * + arithmetic authority), with thin BDT display formatters on top. The package
7
+ * is BD-tenant (BDT / `৳` / `en-BD`); a non-BD deployment forks these three
8
+ * constants. Arithmetic and the `Money` type always come from primitives — this
9
+ * module never re-implements them.
10
+ */
11
+ const TENANT_CURRENCY = "BDT";
12
+ /** Cast a raw number to `Paisa`. Use at SDK boundaries when the response
13
+ * type isn't yet branded, or for arithmetic results that guarantee minor
14
+ * units. One cast per source — not per callsite. */
15
+ function Paisa(n) {
16
+ return MinorUnits(n, TENANT_CURRENCY);
17
+ }
18
+ const TENANT_LOCALE = "en-BD";
19
+ const TENANT_SYMBOL = "৳";
20
+ /** Adapt a paisa (minor-unit) wire value into a structured `Money`. */
21
+ function paisaToMoney(paisa, currency = TENANT_CURRENCY) {
22
+ return money(paisa, currency);
23
+ }
24
+ /** Canonical formatter — structured `Money` → `৳1,234.00`. */
25
+ function formatMoney(m) {
26
+ return new Intl.NumberFormat(TENANT_LOCALE, {
27
+ style: "currency",
28
+ currency: m.currency
29
+ }).format(toMajor(m));
30
+ }
31
+ /** Format a wire-format paisa value: `formatPaisa(489900)` → `৳4,899.00`. */
32
+ function formatPaisa(paisa, currency = TENANT_CURRENCY) {
33
+ return formatMoney(paisaToMoney(paisa, currency));
34
+ }
35
+ /** Compact major-unit formatter, no forced decimals: `formatBdt(4899)` → `৳4,899`. */
36
+ function formatBdt(bdt) {
37
+ return `${TENANT_SYMBOL}${(Number.isFinite(bdt) ? bdt : 0).toLocaleString(TENANT_LOCALE)}`;
38
+ }
39
+
40
+ //#endregion
41
+ export { Paisa, formatBdt, formatMoney, formatPaisa, paisaToMoney };