@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,46 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/runtime/auth-port.d.ts
4
+ /**
5
+ * `PosAuthPort` — the auth seam the POS app needs, injected by the host.
6
+ *
7
+ * POS itself is auth-agnostic: it needs a bearer token for a few manual calls,
8
+ * the current session user, and a *manager override* sign-in (for discount /
9
+ * variance approvals). The host wires these to its own auth client (Better Auth
10
+ * on the web; whatever a desktop shell uses). The package declares the contract,
11
+ * the host supplies the implementation. Types are structural (no better-auth
12
+ * import), so a Tauri / RN shell can implement it against a different client.
13
+ */
14
+ interface PosSessionUser {
15
+ id?: string;
16
+ name?: string;
17
+ email?: string;
18
+ role?: unknown;
19
+ }
20
+ interface PosAuthPort {
21
+ /** Current bearer token (or null). Used for the few manual POS API calls. */
22
+ getToken: () => string | null;
23
+ /** Reactive current session user (null when signed out). */
24
+ useSessionUser: () => PosSessionUser | null;
25
+ /**
26
+ * Manager-override sign-in — validates a manager's credentials WITHOUT
27
+ * replacing the cashier's session (temporary elevation for approvals).
28
+ * Resolves with the authorizing user, rejects on bad credentials / role.
29
+ */
30
+ signInManager: (input: {
31
+ email: string;
32
+ password: string;
33
+ }) => Promise<{
34
+ user: PosSessionUser;
35
+ }>;
36
+ }
37
+ declare function PosAuthProvider({
38
+ auth,
39
+ children
40
+ }: {
41
+ auth: PosAuthPort;
42
+ children: ReactNode;
43
+ }): import("react").JSX.Element;
44
+ declare function usePosAuth(): PosAuthPort;
45
+ //#endregion
46
+ export { PosAuthPort, PosAuthProvider, PosSessionUser, usePosAuth };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+
6
+ //#region src/runtime/auth-port.tsx
7
+ const PosAuthContext = createContext(null);
8
+ function PosAuthProvider({ auth, children }) {
9
+ return /* @__PURE__ */ jsx(PosAuthContext.Provider, {
10
+ value: auth,
11
+ children
12
+ });
13
+ }
14
+ function usePosAuth() {
15
+ const ctx = useContext(PosAuthContext);
16
+ if (!ctx) throw new Error("usePosAuth() must be used within <PosShell> / <PosAuthProvider>. Inject a PosAuthPort (host wires it to its auth client).");
17
+ return ctx;
18
+ }
19
+
20
+ //#endregion
21
+ export { PosAuthProvider, usePosAuth };
@@ -0,0 +1,38 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/runtime/branch-port.d.ts
4
+ /**
5
+ * `PosBranchPort` — the active-branch seam, injected by the host. POS is
6
+ * branch-scoped (products, shifts, receipts) but must NOT depend on the
7
+ * dashboard's branch context (`@classytic/erp-shell/branch`) — that would drag
8
+ * the dashboard stack into a desktop register. So the host provides the active
9
+ * branch: on the web it wires this to `erp-shell`'s `useBranch`; a Tauri/RN
10
+ * register wires its own branch selection.
11
+ */
12
+ interface PosBranch {
13
+ id: string;
14
+ name: string;
15
+ /** Branch role (head_office / sub_branch), if the host distinguishes. */
16
+ role?: string;
17
+ /** Receipt header fields. */
18
+ address?: string;
19
+ bin?: string;
20
+ phone?: string;
21
+ }
22
+ interface PosBranchPort {
23
+ /** Reactive active branch (null before one is selected). */
24
+ useActiveBranch: () => PosBranch | null;
25
+ }
26
+ declare function PosBranchProvider({
27
+ branch,
28
+ children
29
+ }: {
30
+ branch: PosBranchPort;
31
+ children: ReactNode;
32
+ }): import("react").JSX.Element;
33
+ /** Read the injected active-branch port. */
34
+ declare function usePosBranchPort(): PosBranchPort;
35
+ /** Convenience: the active branch directly. */
36
+ declare function usePosBranch(): PosBranch | null;
37
+ //#endregion
38
+ export { PosBranch, PosBranchPort, PosBranchProvider, usePosBranch, usePosBranchPort };
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+
6
+ //#region src/runtime/branch-port.tsx
7
+ const PosBranchContext = createContext(null);
8
+ function PosBranchProvider({ branch, children }) {
9
+ return /* @__PURE__ */ jsx(PosBranchContext.Provider, {
10
+ value: branch,
11
+ children
12
+ });
13
+ }
14
+ /** Read the injected active-branch port. */
15
+ function usePosBranchPort() {
16
+ const ctx = useContext(PosBranchContext);
17
+ if (!ctx) throw new Error("usePosBranchPort() must be used within <PosShell> / <PosBranchProvider>. Inject a PosBranchPort (host wires it to its branch context).");
18
+ return ctx;
19
+ }
20
+ /** Convenience: the active branch directly. */
21
+ function usePosBranch() {
22
+ return usePosBranchPort().useActiveBranch();
23
+ }
24
+
25
+ //#endregion
26
+ export { PosBranchProvider, usePosBranch, usePosBranchPort };
@@ -0,0 +1,24 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/runtime/config.d.ts
4
+ /**
5
+ * `PosRuntime` — host-provided runtime services that are genuinely app/
6
+ * deployment-specific (not auth, not hardware): navigation and money
7
+ * formatting. Injected once by `<PosShell>`; screens read it via
8
+ * `usePosRuntime()`. Keeps the package free of hardcoded routes and currency
9
+ * conventions — the host owns those (same rule as branding elsewhere).
10
+ */
11
+ interface PosRuntime {
12
+ /** Navigate to a host route (e.g. back to `/dashboard`, out to `/sign-in`). */
13
+ onNavigate: (path: string) => void;
14
+ }
15
+ declare function PosRuntimeProvider({
16
+ runtime,
17
+ children
18
+ }: {
19
+ runtime: PosRuntime;
20
+ children: ReactNode;
21
+ }): import("react").JSX.Element;
22
+ declare function usePosRuntime(): PosRuntime;
23
+ //#endregion
24
+ export { PosRuntime, PosRuntimeProvider, usePosRuntime };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+
6
+ //#region src/runtime/config.tsx
7
+ const PosRuntimeContext = createContext(null);
8
+ function PosRuntimeProvider({ runtime, children }) {
9
+ return /* @__PURE__ */ jsx(PosRuntimeContext.Provider, {
10
+ value: runtime,
11
+ children
12
+ });
13
+ }
14
+ function usePosRuntime() {
15
+ const ctx = useContext(PosRuntimeContext);
16
+ if (!ctx) throw new Error("usePosRuntime() must be used within <PosShell> / <PosRuntimeProvider>. Inject a PosRuntime (onNavigate + formatMoney).");
17
+ return ctx;
18
+ }
19
+
20
+ //#endregion
21
+ export { PosRuntimeProvider, usePosRuntime };
@@ -0,0 +1,4 @@
1
+ import { PosAuthPort, PosAuthProvider, PosSessionUser, usePosAuth } from "./auth-port.js";
2
+ import { PosRuntime, PosRuntimeProvider, usePosRuntime } from "./config.js";
3
+ import { PosBranch, PosBranchPort, PosBranchProvider, usePosBranch, usePosBranchPort } from "./branch-port.js";
4
+ export { type PosAuthPort, PosAuthProvider, type PosBranch, type PosBranchPort, PosBranchProvider, type PosRuntime, PosRuntimeProvider, type PosSessionUser, usePosAuth, usePosBranch, usePosBranchPort, usePosRuntime };
@@ -0,0 +1,5 @@
1
+ import { PosAuthProvider, usePosAuth } from "./auth-port.js";
2
+ import { PosRuntimeProvider, usePosRuntime } from "./config.js";
3
+ import { PosBranchProvider, usePosBranch, usePosBranchPort } from "./branch-port.js";
4
+
5
+ export { PosAuthProvider, PosBranchProvider, PosRuntimeProvider, usePosAuth, usePosBranch, usePosBranchPort, usePosRuntime };
@@ -0,0 +1,245 @@
1
+ "use client";
2
+
3
+ import { formatPaisa } from "../lib/money.js";
4
+ import { ReceiptReprintDialog } from "../components/ReceiptReprintDialog.js";
5
+ import { useCallback, useState } from "react";
6
+ import { SheetWrapper } from "@classytic/fluid/client/core";
7
+ import { usePosOrderDetail, usePosOrderHistory, usePosRefundOrder, usePosVoidOrder } from "@classytic/commerce-sdk/sales";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { Ban, ChevronRight, Loader2, Package, Printer, RotateCcw, Search } from "lucide-react";
10
+ import { Button } from "@/components/ui/button";
11
+ import { StatusBadge } from "@classytic/fluid";
12
+ import { Input } from "@/components/ui/input";
13
+
14
+ //#region src/screens/OrderHistoryDrawer.tsx
15
+ /**
16
+ * OrderHistoryDrawer — Slide-out panel with recent POS orders.
17
+ *
18
+ * Powered by SDK hooks (`usePosOrderHistory`, `usePosOrderDetail`,
19
+ * `usePosVoidOrder`, `usePosRefundOrder`) — no inline `handleApiRequest`
20
+ * calls or hand-rolled query keys. Cache invalidation + mutation toasts
21
+ * are handled inside the SDK hooks; this component just composes them
22
+ * with fluid's `SheetWrapper`.
23
+ */
24
+ const STATUS_VARIANT = {
25
+ pending: "warning",
26
+ confirmed: "info",
27
+ processing: "info",
28
+ completed: "success",
29
+ cancelled: "error",
30
+ refunded: "error"
31
+ };
32
+ function OrderHistoryDrawer({ open, onOpenChange }) {
33
+ const [search, setSearch] = useState("");
34
+ const [selectedOrderId, setSelectedOrderId] = useState(null);
35
+ const [reprintOrderId, setReprintOrderId] = useState(null);
36
+ const trimmedSearch = search.trim();
37
+ const { orders, isLoading } = usePosOrderHistory({
38
+ limit: 30,
39
+ ...trimmedSearch ? { search: trimmedSearch } : {}
40
+ }, open);
41
+ const { order: selectedOrder, isLoading: detailLoading } = usePosOrderDetail(selectedOrderId, open);
42
+ const { voidOrder, isPending: isVoiding } = usePosVoidOrder();
43
+ const { refundOrder, isPending: isRefunding } = usePosRefundOrder();
44
+ const handleVoid = useCallback(async () => {
45
+ if (!selectedOrderId || !window.confirm("Void this order?")) return;
46
+ try {
47
+ await voidOrder({
48
+ orderId: selectedOrderId,
49
+ reason: "Voided from POS"
50
+ });
51
+ setSelectedOrderId(null);
52
+ } catch {}
53
+ }, [selectedOrderId, voidOrder]);
54
+ const handleRefund = useCallback(async () => {
55
+ if (!selectedOrderId || !window.confirm("Process full refund?")) return;
56
+ try {
57
+ await refundOrder({
58
+ orderId: selectedOrderId,
59
+ reason: "Full refund from POS"
60
+ });
61
+ setSelectedOrderId(null);
62
+ } catch {}
63
+ }, [selectedOrderId, refundOrder]);
64
+ return /* @__PURE__ */ jsxs(SheetWrapper, {
65
+ open,
66
+ onOpenChange,
67
+ title: "Order History",
68
+ side: "right",
69
+ size: "lg",
70
+ contentClassName: "p-0",
71
+ children: [
72
+ /* @__PURE__ */ jsx("div", {
73
+ className: "px-4 py-2 border-b",
74
+ children: /* @__PURE__ */ jsxs("div", {
75
+ className: "relative",
76
+ children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ jsx(Input, {
77
+ placeholder: "Search by order number...",
78
+ value: search,
79
+ onChange: (e) => setSearch(e.target.value),
80
+ className: "pl-9 h-9"
81
+ })]
82
+ })
83
+ }),
84
+ selectedOrderId && selectedOrder ? /* @__PURE__ */ jsxs("div", {
85
+ className: "flex-1 overflow-y-auto",
86
+ children: [/* @__PURE__ */ jsx("div", {
87
+ className: "px-4 py-3 border-b",
88
+ children: /* @__PURE__ */ jsx(Button, {
89
+ variant: "ghost",
90
+ size: "sm",
91
+ onClick: () => setSelectedOrderId(null),
92
+ className: "h-7 text-xs -ml-2",
93
+ children: "Back to list"
94
+ })
95
+ }), detailLoading ? /* @__PURE__ */ jsx("div", {
96
+ className: "flex items-center justify-center py-12",
97
+ children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" })
98
+ }) : /* @__PURE__ */ jsxs("div", {
99
+ className: "px-4 py-4 space-y-4",
100
+ children: [
101
+ /* @__PURE__ */ jsxs("div", {
102
+ className: "flex items-center justify-between",
103
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h3", {
104
+ className: "font-semibold",
105
+ children: ["#", String(selectedOrder.orderNumber ?? "N/A")]
106
+ }), /* @__PURE__ */ jsx("p", {
107
+ className: "text-xs text-muted-foreground",
108
+ children: new Date(String(selectedOrder.createdAt)).toLocaleString()
109
+ })] }), /* @__PURE__ */ jsx(StatusBadge, {
110
+ variant: STATUS_VARIANT[String(selectedOrder.status)] ?? "neutral",
111
+ children: String(selectedOrder.status)
112
+ })]
113
+ }),
114
+ selectedOrder.customerSnapshot ? /* @__PURE__ */ jsxs("div", {
115
+ className: "text-sm",
116
+ children: [/* @__PURE__ */ jsx("span", {
117
+ className: "text-muted-foreground",
118
+ children: "Customer: "
119
+ }), /* @__PURE__ */ jsx("span", { children: String(selectedOrder.customerSnapshot?.name ?? "Walk-in") })]
120
+ }) : null,
121
+ /* @__PURE__ */ jsxs("div", {
122
+ className: "space-y-2",
123
+ children: [/* @__PURE__ */ jsx("h4", {
124
+ className: "text-sm font-medium",
125
+ children: "Items"
126
+ }), (selectedOrder.lines ?? []).map((line, i) => {
127
+ const lineTotalMoney = line.lineTotal;
128
+ const snapshot = line.snapshot;
129
+ const unitPriceMoney = snapshot?.unitPrice;
130
+ const qty = Number(line.quantity ?? 0);
131
+ const unitPaisa = typeof unitPriceMoney === "number" ? unitPriceMoney : unitPriceMoney?.amount ?? 0;
132
+ const lineTotalPaisa = typeof lineTotalMoney?.amount === "number" ? lineTotalMoney.amount : unitPaisa * qty;
133
+ return /* @__PURE__ */ jsxs("div", {
134
+ className: "flex justify-between text-sm",
135
+ children: [/* @__PURE__ */ jsxs("span", { children: [String(snapshot?.name ?? "Item"), /* @__PURE__ */ jsxs("span", {
136
+ className: "text-muted-foreground ml-1",
137
+ children: ["x", qty]
138
+ })] }), /* @__PURE__ */ jsx("span", {
139
+ className: "tabular-nums",
140
+ children: formatPaisa(lineTotalPaisa)
141
+ })]
142
+ }, i);
143
+ })]
144
+ }),
145
+ /* @__PURE__ */ jsxs("div", {
146
+ className: "flex justify-between font-semibold text-base pt-2 border-t",
147
+ children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", {
148
+ className: "tabular-nums",
149
+ children: formatPaisa(Number((selectedOrder.totals?.grandTotal)?.amount ?? 0))
150
+ })]
151
+ }),
152
+ /* @__PURE__ */ jsxs("div", {
153
+ className: "flex flex-col gap-2 pt-4 border-t",
154
+ children: [
155
+ /* @__PURE__ */ jsxs(Button, {
156
+ variant: "outline",
157
+ size: "sm",
158
+ onClick: () => selectedOrderId && setReprintOrderId(selectedOrderId),
159
+ children: [/* @__PURE__ */ jsx(Printer, { className: "h-4 w-4 mr-2" }), "Reprint Receipt"]
160
+ }),
161
+ (selectedOrder.status === "pending" || selectedOrder.status === "confirmed") && /* @__PURE__ */ jsxs(Button, {
162
+ variant: "outline",
163
+ size: "sm",
164
+ className: "text-destructive",
165
+ onClick: handleVoid,
166
+ disabled: isVoiding,
167
+ children: [/* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mr-2" }), isVoiding ? "Voiding..." : "Void Order"]
168
+ }),
169
+ selectedOrder.status === "completed" && /* @__PURE__ */ jsxs(Button, {
170
+ variant: "outline",
171
+ size: "sm",
172
+ className: "text-orange-600",
173
+ onClick: handleRefund,
174
+ disabled: isRefunding,
175
+ children: [/* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4 mr-2" }), isRefunding ? "Processing..." : "Full Refund"]
176
+ })
177
+ ]
178
+ })
179
+ ]
180
+ })]
181
+ }) : /* @__PURE__ */ jsx("div", {
182
+ className: "flex-1 overflow-y-auto",
183
+ children: isLoading ? /* @__PURE__ */ jsx("div", {
184
+ className: "flex items-center justify-center py-12",
185
+ children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" })
186
+ }) : orders.length === 0 ? /* @__PURE__ */ jsxs("div", {
187
+ className: "flex flex-col items-center justify-center py-12 text-muted-foreground",
188
+ children: [/* @__PURE__ */ jsx(Package, { className: "h-10 w-10 mb-3 opacity-30" }), /* @__PURE__ */ jsx("p", {
189
+ className: "text-sm",
190
+ children: "No POS orders found"
191
+ })]
192
+ }) : /* @__PURE__ */ jsx("div", {
193
+ className: "divide-y",
194
+ children: orders.map((order) => /* @__PURE__ */ jsxs("button", {
195
+ type: "button",
196
+ className: "w-full px-4 py-3 text-left hover:bg-muted/50 transition-colors flex items-center gap-3",
197
+ onClick: () => setSelectedOrderId(String(order.orderNumber ?? order._id)),
198
+ children: [
199
+ /* @__PURE__ */ jsxs("div", {
200
+ className: "flex-1 min-w-0",
201
+ children: [/* @__PURE__ */ jsxs("div", {
202
+ className: "flex items-center gap-2",
203
+ children: [/* @__PURE__ */ jsxs("span", {
204
+ className: "font-medium text-sm",
205
+ children: ["#", String(order.orderNumber ?? "N/A")]
206
+ }), /* @__PURE__ */ jsx(StatusBadge, {
207
+ variant: STATUS_VARIANT[String(order.status)] ?? "neutral",
208
+ size: "sm",
209
+ children: String(order.status)
210
+ })]
211
+ }), /* @__PURE__ */ jsxs("div", {
212
+ className: "flex items-center gap-2 text-xs text-muted-foreground mt-0.5",
213
+ children: [/* @__PURE__ */ jsx("span", { children: new Date(String(order.createdAt)).toLocaleString("en-US", {
214
+ month: "short",
215
+ day: "numeric",
216
+ hour: "2-digit",
217
+ minute: "2-digit"
218
+ }) }), order.customerSnapshot?.name ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: "-" }), /* @__PURE__ */ jsx("span", {
219
+ className: "truncate",
220
+ children: String(order.customerSnapshot.name)
221
+ })] }) : null]
222
+ })]
223
+ }),
224
+ /* @__PURE__ */ jsx("span", {
225
+ className: "font-semibold text-sm tabular-nums shrink-0",
226
+ children: formatPaisa(Number((order.totals?.grandTotal)?.amount ?? 0))
227
+ }),
228
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground shrink-0" })
229
+ ]
230
+ }, String(order.orderNumber ?? order._id)))
231
+ })
232
+ }),
233
+ /* @__PURE__ */ jsx(ReceiptReprintDialog, {
234
+ order: reprintOrderId && selectedOrder ? selectedOrder : null,
235
+ open: !!reprintOrderId,
236
+ onOpenChange: (o) => {
237
+ if (!o) setReprintOrderId(null);
238
+ }
239
+ })
240
+ ]
241
+ });
242
+ }
243
+
244
+ //#endregion
245
+ export { OrderHistoryDrawer };
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { formatBdt } from "../lib/money.js";
4
+ import { calculateCartTotals } from "../utils/pos-helpers.js";
5
+ import { useCallback } from "react";
6
+ import { SheetWrapper } from "@classytic/fluid/client/core";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Inbox, PlayCircle, Trash2 } from "lucide-react";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Badge } from "@/components/ui/badge";
11
+
12
+ //#region src/screens/ParkedOrdersDrawer.tsx
13
+ /**
14
+ * ParkedOrdersDrawer — slide-out list of parked sales.
15
+ *
16
+ * Backed by `usePosMultiOrder` (localStorage, max 5 slots, 8h TTL per
17
+ * branch). Resume drops the parked cart back into the active session and
18
+ * removes the slot. Delete discards without resuming.
19
+ *
20
+ * Use case: cashier mid-sale, customer steps away to grab one more item;
21
+ * cashier parks the sale, serves the next customer, resumes when the
22
+ * first returns. Same pattern as Square "Save sale" / Lightspeed "On hold".
23
+ */
24
+ function formatRelative(timestamp) {
25
+ const diffMs = Date.now() - timestamp;
26
+ const diffMin = Math.floor(diffMs / 6e4);
27
+ if (diffMin < 1) return "just now";
28
+ if (diffMin < 60) return `${diffMin} min ago`;
29
+ return `${Math.floor(diffMin / 60)}h ${diffMin % 60}m ago`;
30
+ }
31
+ function ParkedOrdersDrawer({ open, onOpenChange, orders, onResume, onDelete }) {
32
+ const handleDelete = useCallback((id) => {
33
+ if (window.confirm("Discard this parked sale?")) onDelete(id);
34
+ }, [onDelete]);
35
+ return /* @__PURE__ */ jsx(SheetWrapper, {
36
+ open,
37
+ onOpenChange,
38
+ title: "Parked sales",
39
+ description: "Hold an in-progress sale and resume later. Max 5 per branch.",
40
+ side: "right",
41
+ size: "default",
42
+ contentClassName: "p-0",
43
+ children: /* @__PURE__ */ jsx("div", {
44
+ className: "flex-1 overflow-y-auto",
45
+ children: orders.length === 0 ? /* @__PURE__ */ jsxs("div", {
46
+ className: "flex flex-col items-center justify-center py-12 text-muted-foreground",
47
+ children: [
48
+ /* @__PURE__ */ jsx(Inbox, { className: "h-10 w-10 mb-3 opacity-30" }),
49
+ /* @__PURE__ */ jsx("p", {
50
+ className: "text-sm",
51
+ children: "No parked sales"
52
+ }),
53
+ /* @__PURE__ */ jsxs("p", {
54
+ className: "text-xs mt-1",
55
+ children: [
56
+ "Press ",
57
+ /* @__PURE__ */ jsx("kbd", {
58
+ className: "px-1.5 py-0.5 rounded border text-[10px]",
59
+ children: "F6"
60
+ }),
61
+ " on a sale to park it"
62
+ ]
63
+ })
64
+ ]
65
+ }) : /* @__PURE__ */ jsx("div", {
66
+ className: "divide-y",
67
+ children: orders.map((order) => {
68
+ const totals = calculateCartTotals(order.cart, order.discountInput);
69
+ const itemCount = order.cart.reduce((sum, item) => sum + item.quantity, 0);
70
+ return /* @__PURE__ */ jsxs("div", {
71
+ className: "px-4 py-3",
72
+ children: [/* @__PURE__ */ jsxs("div", {
73
+ className: "flex items-start justify-between gap-2 mb-2",
74
+ children: [/* @__PURE__ */ jsxs("div", {
75
+ className: "min-w-0 flex-1",
76
+ children: [/* @__PURE__ */ jsxs("div", {
77
+ className: "flex items-center gap-2 flex-wrap",
78
+ children: [/* @__PURE__ */ jsx("span", {
79
+ className: "font-medium text-sm truncate",
80
+ children: order.label
81
+ }), /* @__PURE__ */ jsxs(Badge, {
82
+ variant: "secondary",
83
+ className: "text-[10px]",
84
+ children: [
85
+ itemCount,
86
+ " ",
87
+ itemCount === 1 ? "item" : "items"
88
+ ]
89
+ })]
90
+ }), /* @__PURE__ */ jsxs("p", {
91
+ className: "text-xs text-muted-foreground mt-0.5",
92
+ children: [formatRelative(order.parkedAt), order.customerName && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
93
+ className: "mx-1",
94
+ children: "·"
95
+ }), /* @__PURE__ */ jsx("span", {
96
+ className: "truncate",
97
+ children: order.customerName
98
+ })] })]
99
+ })]
100
+ }), /* @__PURE__ */ jsx("span", {
101
+ className: "text-sm font-semibold tabular-nums shrink-0",
102
+ children: formatBdt(totals.total)
103
+ })]
104
+ }), /* @__PURE__ */ jsxs("div", {
105
+ className: "flex gap-2",
106
+ children: [/* @__PURE__ */ jsxs(Button, {
107
+ size: "sm",
108
+ className: "h-8 flex-1",
109
+ onClick: () => onResume(order.id),
110
+ children: [/* @__PURE__ */ jsx(PlayCircle, { className: "h-3.5 w-3.5 mr-1.5" }), "Resume"]
111
+ }), /* @__PURE__ */ jsx(Button, {
112
+ variant: "outline",
113
+ size: "sm",
114
+ className: "h-8 text-destructive",
115
+ onClick: () => handleDelete(order.id),
116
+ "aria-label": "Discard parked sale",
117
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
118
+ })]
119
+ })]
120
+ }, order.id);
121
+ })
122
+ })
123
+ })
124
+ });
125
+ }
126
+
127
+ //#endregion
128
+ export { ParkedOrdersDrawer };