@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.
- package/README.md +38 -0
- package/dist/components/CartSidebar.js +222 -0
- package/dist/components/PosTopBar.js +156 -0
- package/dist/components/PrinterSettingsDialog.js +163 -0
- package/dist/components/ReceiptReprintDialog.js +182 -0
- package/dist/dashboard/components/CustomerLookupDialog.js +120 -0
- package/dist/dashboard/components/CustomerQuickAddDialog.js +101 -0
- package/dist/dashboard/components/ManagerAuthDialog.js +77 -0
- package/dist/dashboard/components/ProductCard.js +98 -0
- package/dist/dashboard/components/ProductsPanel.js +119 -0
- package/dist/dashboard/components/SplitPaymentPanel.js +131 -0
- package/dist/dashboard/components/VariantSelectorDialog.js +150 -0
- package/dist/dashboard/components/cart/AddChargeDialog.js +99 -0
- package/dist/dashboard/components/cart/CartItems.js +103 -0
- package/dist/dashboard/components/cart/CartSummary.js +73 -0
- package/dist/dashboard/components/cart/CustomerSection.js +127 -0
- package/dist/dashboard/components/cart/DiscountSection.js +50 -0
- package/dist/dashboard/components/cart/PointsRedemptionSection.js +58 -0
- package/dist/hardware/context.d.ts +15 -0
- package/dist/hardware/context.js +33 -0
- package/dist/hardware/index.d.ts +7 -0
- package/dist/hardware/index.js +7 -0
- package/dist/hardware/ports.d.ts +116 -0
- package/dist/hardware/tauri-adapter.d.ts +7 -0
- package/dist/hardware/tauri-adapter.js +77 -0
- package/dist/hardware/use-printer-availability.d.ts +12 -0
- package/dist/hardware/use-printer-availability.js +50 -0
- package/dist/hardware/use-printer-config.d.ts +16 -0
- package/dist/hardware/use-printer-config.js +62 -0
- package/dist/hardware/web-adapter.d.ts +15 -0
- package/dist/hardware/web-adapter.js +80 -0
- package/dist/hooks/useManagerAuth.js +83 -0
- package/dist/hooks/usePosCart.js +142 -0
- package/dist/hooks/usePosCustomer.js +192 -0
- package/dist/hooks/usePosMultiOrder.js +48 -0
- package/dist/hooks/usePosPayment.js +194 -0
- package/dist/lib/cn.js +12 -0
- package/dist/lib/loyalty.js +30 -0
- package/dist/lib/money.js +41 -0
- package/dist/node_modules/react-hook-form/dist/index.esm.js +1661 -0
- package/dist/runtime/auth-port.d.ts +46 -0
- package/dist/runtime/auth-port.js +21 -0
- package/dist/runtime/branch-port.d.ts +38 -0
- package/dist/runtime/branch-port.js +26 -0
- package/dist/runtime/config.d.ts +24 -0
- package/dist/runtime/config.js +21 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +5 -0
- package/dist/screens/OrderHistoryDrawer.js +245 -0
- package/dist/screens/ParkedOrdersDrawer.js +128 -0
- package/dist/screens/PaymentScreen.js +397 -0
- package/dist/screens/ProductScreen.js +322 -0
- package/dist/screens/ReceiptScreen.js +288 -0
- package/dist/screens/ShiftCloseScreen.js +365 -0
- package/dist/screens/ShiftOpenScreen.js +176 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/pos-shell.d.ts +23 -0
- package/dist/shell/pos-shell.js +132 -0
- package/dist/state/pos-context.js +33 -0
- package/dist/state/pos-state.js +70 -0
- package/dist/utils/customer-display.js +35 -0
- package/dist/utils/pos-helpers.js +242 -0
- package/package.json +92 -0
- 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 };
|