@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,397 @@
1
+ "use client";
2
+
3
+ import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { usePosBranch } from "../runtime/branch-port.js";
5
+ import { usePosContext } from "../state/pos-context.js";
6
+ import { usePosCart } from "../hooks/usePosCart.js";
7
+ import { formatBdt } from "../lib/money.js";
8
+ import { calculateOrderTotals, isValidBDPhone, parsePositiveNumber, transformOrderToReceipt } from "../utils/pos-helpers.js";
9
+ import { usePosCustomer } from "../hooks/usePosCustomer.js";
10
+ import { paymentNeedsReference, usePosPayment } from "../hooks/usePosPayment.js";
11
+ import { SplitPaymentPanel } from "../dashboard/components/SplitPaymentPanel.js";
12
+ import { useCallback, useMemo, useState } from "react";
13
+ import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
14
+ import { ResponsiveSplitLayout } from "@classytic/fluid/client/core";
15
+ import { generateIdempotencyKey, usePosOrders } from "@classytic/commerce-sdk/sales";
16
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
+ import { ArrowLeft, Banknote, CreditCard, Loader2, ReceiptText } from "lucide-react";
18
+ import { Button } from "@/components/ui/button";
19
+ import { useMembershipConfig } from "@classytic/commerce-sdk/platform";
20
+ import { toast } from "sonner";
21
+ import { Input } from "@/components/ui/input";
22
+ import { Numpad } from "@classytic/fluid/numpad";
23
+
24
+ //#region src/screens/PaymentScreen.tsx
25
+ /**
26
+ * PaymentScreen — Dedicated full-screen payment.
27
+ *
28
+ * Left: Order summary (items, totals, customer)
29
+ * Right: Payment method selection + numpad for cash + reference inputs
30
+ */
31
+ const CASH_SUGGESTIONS = [
32
+ 50,
33
+ 100,
34
+ 200,
35
+ 500,
36
+ 1e3,
37
+ 2e3
38
+ ];
39
+ function PaymentScreen() {
40
+ const token = usePosAuth().getToken() || "";
41
+ const selectedBranch = usePosBranch();
42
+ const { dispatch } = usePosContext();
43
+ const branchId = selectedBranch?.id;
44
+ const cart = usePosCart(branchId);
45
+ const customer = usePosCustomer(token);
46
+ const { membershipConfig: membership } = useMembershipConfig();
47
+ const orderTotals = useMemo(() => calculateOrderTotals({
48
+ items: cart.cart,
49
+ manualDiscountInput: cart.discountInput,
50
+ membershipConfig: membership,
51
+ customer: customer.selectedCustomer,
52
+ pointsToRedeemInput: cart.pointsToRedeemInput
53
+ }), [
54
+ cart.cart,
55
+ cart.discountInput,
56
+ membership,
57
+ customer.selectedCustomer,
58
+ cart.pointsToRedeemInput
59
+ ]);
60
+ const payment = usePosPayment(token, orderTotals.total);
61
+ const { createOrder, isCreatingOrder } = usePosOrders();
62
+ const [cashInput, setCashInput] = useState("");
63
+ useKeyboardShortcut("escape", () => dispatch({ type: "GO_BACK" }), {
64
+ disableInInputs: false,
65
+ description: "Back to cart",
66
+ category: "Payment"
67
+ });
68
+ useKeyboardShortcut("f8", () => {
69
+ payment.setMode(payment.state.mode === "split" ? "single" : "split");
70
+ }, {
71
+ description: "Toggle split-payment mode",
72
+ category: "Payment"
73
+ });
74
+ useKeyboardShortcut("f1", () => {
75
+ if (payment.state.mode !== "single") return;
76
+ const cashOption = payment.options.find((o) => o.posMethod === "cash");
77
+ if (cashOption) payment.selectPayment(cashOption.key);
78
+ }, {
79
+ description: "Select cash payment",
80
+ category: "Payment"
81
+ });
82
+ const handleCashSuggestion = useCallback((amount) => {
83
+ setCashInput(String(amount));
84
+ payment.setCashReceived(String(amount));
85
+ }, [payment]);
86
+ const handleCheckout = useCallback(async () => {
87
+ if (!branchId) return;
88
+ let paymentPayload = {};
89
+ if (payment.state.mode === "split") {
90
+ if (!payment.validateAllSplits()) {
91
+ toast.error("Please fix split payment errors");
92
+ return;
93
+ }
94
+ const payments = payment.state.splitEntries.map((entry) => ({
95
+ method: entry.posMethod,
96
+ amount: parsePositiveNumber(entry.amount),
97
+ ...entry.reference.trim() && { reference: entry.reference.trim() }
98
+ }));
99
+ const splitTotal = payments.reduce((sum, p) => sum + p.amount, 0);
100
+ if (Math.abs(splitTotal - orderTotals.total) > .01) {
101
+ toast.error("Split payment total must match order total");
102
+ return;
103
+ }
104
+ paymentPayload = { payments };
105
+ } else {
106
+ if (paymentNeedsReference(payment.state.selectedMethod)) {
107
+ if (!payment.state.reference.trim()) {
108
+ toast.error("Payment reference is required");
109
+ return;
110
+ }
111
+ } else {
112
+ if (!payment.state.cashReceived.trim()) {
113
+ toast.error("Enter cash received");
114
+ return;
115
+ }
116
+ if (payment.state.amountDue > 0) {
117
+ toast.error("Insufficient cash received");
118
+ return;
119
+ }
120
+ }
121
+ paymentPayload = { payments: [{
122
+ method: payment.state.selectedMethod,
123
+ amount: orderTotals.total,
124
+ ...paymentNeedsReference(payment.state.selectedMethod) && { reference: payment.state.reference.trim() }
125
+ }] };
126
+ }
127
+ try {
128
+ const orderData = {
129
+ items: cart.cart.map((item) => {
130
+ if (item.kind === "service-fee") return {
131
+ kind: "service-fee",
132
+ label: item.productName,
133
+ quantity: item.quantity,
134
+ price: item.unitPrice
135
+ };
136
+ return {
137
+ productId: item.productId,
138
+ variantSku: item.variantSku,
139
+ quantity: item.quantity,
140
+ price: item.unitPrice
141
+ };
142
+ }),
143
+ branchId,
144
+ deliveryMethod: "pickup",
145
+ ...cart.membershipCardId.trim() && { membershipCardId: cart.membershipCardId.trim() },
146
+ customer: {
147
+ name: customer.customerName.trim() || "Walk-in Customer",
148
+ ...customer.customerPhone.trim() && isValidBDPhone(customer.customerPhone.trim()) && { phone: customer.customerPhone.trim() },
149
+ ...customer.selectedCustomer?._id && { id: customer.selectedCustomer._id }
150
+ },
151
+ ...paymentPayload,
152
+ ...orderTotals.manualDiscount > 0 && { discount: orderTotals.manualDiscount },
153
+ ...orderTotals.pointsRedeemed > 0 && { pointsToRedeem: orderTotals.pointsRedeemed },
154
+ idempotencyKey: generateIdempotencyKey()
155
+ };
156
+ const receipt = transformOrderToReceipt(await createOrder(orderData), {
157
+ name: selectedBranch?.name || "Store",
158
+ phone: selectedBranch?.phone
159
+ });
160
+ cart.resetCart();
161
+ payment.reset();
162
+ customer.resetCustomer();
163
+ dispatch({
164
+ type: "COMPLETE_SALE",
165
+ receipt
166
+ });
167
+ toast.success("Order completed!");
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : "Failed to create order";
170
+ toast.error(message);
171
+ }
172
+ }, [
173
+ branchId,
174
+ selectedBranch,
175
+ cart,
176
+ customer,
177
+ payment,
178
+ orderTotals,
179
+ createOrder,
180
+ dispatch
181
+ ]);
182
+ const isCash = !paymentNeedsReference(payment.state.selectedMethod);
183
+ return /* @__PURE__ */ jsx(ResponsiveSplitLayout, {
184
+ className: "h-full",
185
+ defaultLayout: [50, 50],
186
+ minSizes: [30, 30],
187
+ persistLayoutKey: "pos-payment-split",
188
+ mobileVariant: "tabs",
189
+ mobileTabsPreset: "line",
190
+ leftPanel: {
191
+ title: "Summary",
192
+ icon: /* @__PURE__ */ jsx(ReceiptText, { className: "h-4 w-4" }),
193
+ content: /* @__PURE__ */ jsxs("div", {
194
+ className: "flex h-full flex-col min-w-0",
195
+ children: [
196
+ /* @__PURE__ */ jsxs("div", {
197
+ className: "flex items-center gap-3 px-6 py-4 border-b",
198
+ children: [/* @__PURE__ */ jsxs(Button, {
199
+ variant: "ghost",
200
+ size: "sm",
201
+ onClick: () => dispatch({ type: "GO_BACK" }),
202
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Back"]
203
+ }), /* @__PURE__ */ jsx("h2", {
204
+ className: "font-semibold text-lg",
205
+ children: "Order Summary"
206
+ })]
207
+ }),
208
+ /* @__PURE__ */ jsx("div", {
209
+ className: "flex-1 overflow-y-auto px-6 py-4 space-y-3",
210
+ children: cart.cart.map((item, i) => /* @__PURE__ */ jsxs("div", {
211
+ className: "flex justify-between text-sm",
212
+ children: [/* @__PURE__ */ jsxs("div", {
213
+ className: "min-w-0",
214
+ children: [
215
+ /* @__PURE__ */ jsx("span", {
216
+ className: "font-medium",
217
+ children: item.productName
218
+ }),
219
+ item.variantLabel && /* @__PURE__ */ jsxs("span", {
220
+ className: "text-muted-foreground ml-1",
221
+ children: [
222
+ "(",
223
+ item.variantLabel,
224
+ ")"
225
+ ]
226
+ }),
227
+ /* @__PURE__ */ jsxs("span", {
228
+ className: "text-muted-foreground ml-2",
229
+ children: ["x", item.quantity]
230
+ })
231
+ ]
232
+ }), /* @__PURE__ */ jsx("span", {
233
+ className: "font-medium tabular-nums shrink-0 ml-4",
234
+ children: formatBdt(item.lineTotal)
235
+ })]
236
+ }, `${item.productId}-${item.variantSku || ""}`))
237
+ }),
238
+ /* @__PURE__ */ jsxs("div", {
239
+ className: "px-6 py-4 border-t space-y-2",
240
+ children: [
241
+ /* @__PURE__ */ jsxs("div", {
242
+ className: "flex justify-between text-sm",
243
+ children: [/* @__PURE__ */ jsx("span", { children: "Subtotal" }), /* @__PURE__ */ jsx("span", {
244
+ className: "tabular-nums",
245
+ children: formatBdt(orderTotals.subtotal)
246
+ })]
247
+ }),
248
+ orderTotals.manualDiscount > 0 && /* @__PURE__ */ jsxs("div", {
249
+ className: "flex justify-between text-sm text-destructive",
250
+ children: [/* @__PURE__ */ jsx("span", { children: "Discount" }), /* @__PURE__ */ jsxs("span", {
251
+ className: "tabular-nums",
252
+ children: ["-", formatBdt(orderTotals.manualDiscount)]
253
+ })]
254
+ }),
255
+ orderTotals.tierDiscount > 0 && /* @__PURE__ */ jsxs("div", {
256
+ className: "flex justify-between text-sm text-blue-600",
257
+ children: [/* @__PURE__ */ jsxs("span", { children: [
258
+ "Tier Discount (",
259
+ orderTotals.tierName,
260
+ ")"
261
+ ] }), /* @__PURE__ */ jsxs("span", {
262
+ className: "tabular-nums",
263
+ children: ["-", formatBdt(orderTotals.tierDiscount)]
264
+ })]
265
+ }),
266
+ /* @__PURE__ */ jsxs("div", {
267
+ className: "flex justify-between text-lg font-bold pt-2 border-t",
268
+ children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", {
269
+ className: "tabular-nums",
270
+ children: formatBdt(orderTotals.total)
271
+ })]
272
+ })
273
+ ]
274
+ })
275
+ ]
276
+ })
277
+ },
278
+ rightPanel: {
279
+ title: "Payment",
280
+ icon: /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
281
+ content: /* @__PURE__ */ jsxs("div", {
282
+ className: "flex h-full flex-col min-w-0",
283
+ children: [
284
+ /* @__PURE__ */ jsx("div", {
285
+ className: "px-6 py-4 border-b",
286
+ children: /* @__PURE__ */ jsxs("h2", {
287
+ className: "font-semibold text-lg flex items-center gap-2",
288
+ children: [/* @__PURE__ */ jsx(CreditCard, { className: "h-5 w-5" }), "Payment"]
289
+ })
290
+ }),
291
+ /* @__PURE__ */ jsxs("div", {
292
+ className: "flex-1 overflow-y-auto px-6 py-4 space-y-4",
293
+ children: [
294
+ payment.state.mode !== "split" && /* @__PURE__ */ jsx("div", {
295
+ className: "grid grid-cols-2 gap-2",
296
+ children: payment.options.map((opt) => /* @__PURE__ */ jsxs(Button, {
297
+ variant: payment.state.selectedKey === opt.key ? "default" : "outline",
298
+ className: "h-12 text-sm",
299
+ 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]
301
+ }, opt.key))
302
+ }),
303
+ /* @__PURE__ */ jsxs("div", {
304
+ className: "flex gap-2",
305
+ children: [/* @__PURE__ */ jsx(Button, {
306
+ variant: payment.state.mode === "single" ? "default" : "outline",
307
+ size: "sm",
308
+ onClick: () => payment.setMode("single"),
309
+ children: "Single"
310
+ }), /* @__PURE__ */ jsx(Button, {
311
+ variant: payment.state.mode === "split" ? "default" : "outline",
312
+ size: "sm",
313
+ onClick: () => payment.setMode("split"),
314
+ children: "Split (F8)"
315
+ })]
316
+ }),
317
+ payment.state.mode === "split" ? /* @__PURE__ */ jsx(SplitPaymentPanel, {
318
+ entries: payment.state.splitEntries,
319
+ options: payment.options,
320
+ total: orderTotals.total,
321
+ remaining: payment.state.splitRemaining,
322
+ onAdd: payment.addSplit,
323
+ onUpdate: payment.updateSplit,
324
+ onRemove: payment.removeSplit
325
+ }) : isCash ? /* @__PURE__ */ jsxs(Fragment, { children: [
326
+ /* @__PURE__ */ jsxs("div", {
327
+ className: "grid grid-cols-3 gap-2",
328
+ children: [/* @__PURE__ */ jsx(Button, {
329
+ variant: "outline",
330
+ size: "sm",
331
+ onClick: () => handleCashSuggestion(Math.ceil(orderTotals.total)),
332
+ children: "Exact"
333
+ }), CASH_SUGGESTIONS.filter((s) => s >= orderTotals.total).slice(0, 5).map((amount) => /* @__PURE__ */ jsx(Button, {
334
+ variant: "outline",
335
+ size: "sm",
336
+ onClick: () => handleCashSuggestion(amount),
337
+ children: amount
338
+ }, amount))]
339
+ }),
340
+ /* @__PURE__ */ jsx(Numpad, {
341
+ value: cashInput,
342
+ onChange: (v) => {
343
+ setCashInput(v);
344
+ payment.setCashReceived(v);
345
+ },
346
+ onSubmit: isCreatingOrder ? void 0 : handleCheckout,
347
+ submitLabel: `Pay ${formatBdt(orderTotals.total)}`,
348
+ quickAdd: [
349
+ 100,
350
+ 200,
351
+ 500,
352
+ 1e3
353
+ ],
354
+ density: "touch",
355
+ decimal: true
356
+ }),
357
+ payment.state.changeAmount > 0 && /* @__PURE__ */ jsxs("div", {
358
+ className: "bg-green-50 dark:bg-green-950 rounded-lg p-4 text-center",
359
+ children: [/* @__PURE__ */ jsx("p", {
360
+ className: "text-sm text-muted-foreground",
361
+ children: "Change"
362
+ }), /* @__PURE__ */ jsx("p", {
363
+ className: "text-2xl font-bold text-green-600 tabular-nums",
364
+ children: formatBdt(payment.state.changeAmount)
365
+ })]
366
+ })
367
+ ] }) : /* @__PURE__ */ jsxs("div", {
368
+ className: "space-y-3",
369
+ children: [/* @__PURE__ */ jsx(Input, {
370
+ placeholder: `Enter ${payment.selectedOption?.label || "payment"} reference`,
371
+ value: payment.state.reference,
372
+ onChange: (e) => payment.setReference(e.target.value),
373
+ autoFocus: true
374
+ }), payment.selectedOption?.walletNumber && /* @__PURE__ */ jsxs("p", {
375
+ className: "text-xs text-muted-foreground",
376
+ children: ["Send to: ", payment.selectedOption.walletNumber]
377
+ })]
378
+ })
379
+ ]
380
+ }),
381
+ (payment.state.mode === "split" || !isCash) && /* @__PURE__ */ jsx("div", {
382
+ className: "px-6 py-4 border-t",
383
+ children: /* @__PURE__ */ jsx(Button, {
384
+ className: "w-full h-12 text-base font-semibold",
385
+ onClick: handleCheckout,
386
+ 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)}`
388
+ })
389
+ })
390
+ ]
391
+ })
392
+ }
393
+ });
394
+ }
395
+
396
+ //#endregion
397
+ export { PaymentScreen };