@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,365 @@
1
+ "use client";
2
+
3
+ import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { usePosContext } from "../state/pos-context.js";
5
+ import { formatPaisa } from "../lib/money.js";
6
+ import { useManagerAuth } from "../hooks/useManagerAuth.js";
7
+ import { ManagerAuthDialog } from "../dashboard/components/ManagerAuthDialog.js";
8
+ import { useCallback, useMemo, useState } from "react";
9
+ import { useBlindCloseShift, useCloseShift, usePosCurrentShift } from "@classytic/commerce-sdk/sales";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { AlertCircle, AlertTriangle, ArrowLeft, CheckCircle2, ClipboardList, ShieldCheck } from "lucide-react";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Label } from "@/components/ui/label";
15
+ import { Numpad } from "@classytic/fluid/numpad";
16
+
17
+ //#region src/screens/ShiftCloseScreen.tsx
18
+ /**
19
+ * ShiftCloseScreen — Cash reconciliation form with supervisor-override flow.
20
+ *
21
+ * 1. Cashier types closing-cash count.
22
+ * 2. Variance shown immediately (over / short / exceeds threshold).
23
+ * 3. If variance > policy threshold AND `policy.managerOverrideRequired`,
24
+ * the close button is replaced with **"Get supervisor authorization"**.
25
+ * 4. Click → fluid `ManagerAuthDialog` opens; supervisor authenticates with
26
+ * Better Auth (their *own* credentials — different user than cashier).
27
+ * 5. After auth, supervisor types the override reason and confirms close.
28
+ * The mutation sends `varianceOverride: { overriddenBy: supervisor.id,
29
+ * reason }` so the audit trail records the supervisor as the
30
+ * authority — NOT the cashier.
31
+ * 6. Self-close (cashier IS a manager) is allowed — they don't need to
32
+ * authenticate themselves; their session role already qualifies.
33
+ *
34
+ * Wired to backend via `useCloseShift` + `useBlindCloseShift`. Variance
35
+ * gate is evaluated server-side; the FE does the same calculation locally
36
+ * for proactive UX (so the cashier sees what they're about to do).
37
+ */
38
+ const SUPERVISOR_ROLES = [
39
+ "superadmin",
40
+ "admin",
41
+ "branch_manager"
42
+ ];
43
+ function rolesArray(role) {
44
+ if (Array.isArray(role)) return role.filter((r) => typeof r === "string");
45
+ if (typeof role === "string") return [role];
46
+ return [];
47
+ }
48
+ function ShiftCloseScreen() {
49
+ const { dispatch } = usePosContext();
50
+ const { shift } = usePosCurrentShift();
51
+ const { closeShift, isPending: isClosingDirect, error: closeErr } = useCloseShift();
52
+ const { blindCloseShift, isPending: isBlindClosing, error: blindErr } = useBlindCloseShift();
53
+ const sessionUser = usePosAuth().useSessionUser();
54
+ const supervisorAuth = useManagerAuth({ allowedRoles: SUPERVISOR_ROLES });
55
+ const [closingCash, setClosingCash] = useState("");
56
+ const [overrideReason, setOverrideReason] = useState("");
57
+ const [authDialogOpen, setAuthDialogOpen] = useState(false);
58
+ const [submitError, setSubmitError] = useState(null);
59
+ const isPending = isClosingDirect || isBlindClosing;
60
+ const cashRow = shift?.paymentBreakdown?.find((r) => r.method === "cash");
61
+ const liveExpectedCash = cashRow ? (cashRow.openingAmount ?? 0) + (cashRow.salesAmount ?? 0) - (cashRow.refundAmount ?? 0) + (cashRow.cashInAmount ?? 0) - (cashRow.cashOutAmount ?? 0) : shift?.openingCash ?? 0;
62
+ const expectedCash = shift?.expectedCash ?? liveExpectedCash;
63
+ const salesCount = shift?.salesCount ?? 0;
64
+ const salesTotal = shift?.salesTotal ?? 0;
65
+ const actualCash = Math.round((parseFloat(closingCash) || 0) * 100);
66
+ const difference = closingCash ? actualCash - expectedCash : 0;
67
+ const varianceAbs = Math.abs(difference);
68
+ const policy = shift?.policySnapshot;
69
+ const blindCloseRequired = policy?.blindCloseRequired ?? false;
70
+ const varianceOverThreshold = useMemo(() => {
71
+ if (!policy || !closingCash) return false;
72
+ const withinAbs = varianceAbs <= policy.varianceThresholdAbs;
73
+ const withinPct = expectedCash === 0 ? false : varianceAbs / Math.abs(expectedCash) * 100 <= policy.varianceThresholdPct;
74
+ return !(withinAbs || withinPct);
75
+ }, [
76
+ varianceAbs,
77
+ expectedCash,
78
+ policy,
79
+ closingCash
80
+ ]);
81
+ const overrideRequired = varianceOverThreshold && (policy?.managerOverrideRequired ?? true);
82
+ const cashierIsSupervisor = rolesArray(sessionUser?.role).some((r) => SUPERVISOR_ROLES.includes(r));
83
+ const supervisorId = supervisorAuth.authorizedBy?.id;
84
+ const supervisorName = supervisorAuth.authorizedBy?.name;
85
+ const supervisorReady = supervisorAuth.isAuthorized && Boolean(supervisorId);
86
+ const canSubmit = !!closingCash && !isPending && (!overrideRequired || cashierIsSupervisor || supervisorReady && overrideReason.trim().length > 0);
87
+ const handleSubmit = useCallback(async () => {
88
+ setSubmitError(null);
89
+ if (!shift) {
90
+ setSubmitError("No active shift found");
91
+ return;
92
+ }
93
+ try {
94
+ if (blindCloseRequired) await blindCloseShift({
95
+ id: shift._id,
96
+ data: { countedCash: actualCash }
97
+ });
98
+ else if (overrideRequired && !cashierIsSupervisor) {
99
+ if (!supervisorReady) {
100
+ setSubmitError("Supervisor authorization required");
101
+ setAuthDialogOpen(true);
102
+ return;
103
+ }
104
+ if (!overrideReason.trim()) {
105
+ setSubmitError("Reason required for supervisor override");
106
+ return;
107
+ }
108
+ await closeShift({
109
+ id: shift._id,
110
+ data: {
111
+ countedCash: actualCash,
112
+ varianceOverride: {
113
+ overriddenBy: supervisorId,
114
+ reason: overrideReason.trim()
115
+ }
116
+ }
117
+ });
118
+ } else await closeShift({
119
+ id: shift._id,
120
+ data: {
121
+ countedCash: actualCash,
122
+ ...overrideRequired && overrideReason.trim() ? { managerOverrideReason: overrideReason.trim() } : {}
123
+ }
124
+ });
125
+ dispatch({ type: "CONFIRM_CLOSE" });
126
+ } catch (err) {
127
+ const msg = err.message || "Failed to close register";
128
+ setSubmitError(msg);
129
+ if (msg.toLowerCase().includes("variance")) setAuthDialogOpen(true);
130
+ }
131
+ }, [
132
+ shift,
133
+ blindCloseRequired,
134
+ blindCloseShift,
135
+ closeShift,
136
+ overrideRequired,
137
+ cashierIsSupervisor,
138
+ supervisorReady,
139
+ supervisorId,
140
+ overrideReason,
141
+ actualCash,
142
+ dispatch
143
+ ]);
144
+ const handleCancel = useCallback(() => {
145
+ dispatch({ type: "CANCEL_CLOSE" });
146
+ }, [dispatch]);
147
+ const submitLabel = blindCloseRequired ? "Blind-Close Register" : overrideRequired && !cashierIsSupervisor && !supervisorReady ? "Get Supervisor Authorization" : "Close Register";
148
+ const onSubmitHandler = useCallback(() => {
149
+ if (overrideRequired && !cashierIsSupervisor && !supervisorReady) {
150
+ setAuthDialogOpen(true);
151
+ return;
152
+ }
153
+ handleSubmit();
154
+ }, [
155
+ overrideRequired,
156
+ cashierIsSupervisor,
157
+ supervisorReady,
158
+ handleSubmit
159
+ ]);
160
+ return /* @__PURE__ */ jsxs("div", {
161
+ className: "flex items-center justify-center h-full bg-muted/30",
162
+ children: [/* @__PURE__ */ jsxs("div", {
163
+ className: "w-full max-w-lg bg-card rounded-xl shadow-lg p-8 space-y-6",
164
+ children: [
165
+ /* @__PURE__ */ jsxs("div", {
166
+ className: "flex items-center justify-between",
167
+ children: [
168
+ /* @__PURE__ */ jsxs(Button, {
169
+ variant: "ghost",
170
+ size: "sm",
171
+ onClick: handleCancel,
172
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Cancel"]
173
+ }),
174
+ /* @__PURE__ */ jsxs("h1", {
175
+ className: "text-xl font-bold flex items-center gap-2",
176
+ children: [/* @__PURE__ */ jsx(ClipboardList, { className: "h-5 w-5" }), /* @__PURE__ */ jsxs("span", { children: ["Closing Shift", shift?.openedAt ? /* @__PURE__ */ jsxs("span", {
177
+ className: "text-sm font-medium text-muted-foreground ml-2",
178
+ children: [
179
+ "· opened",
180
+ " ",
181
+ new Date(shift.openedAt).toLocaleTimeString("en-GB", {
182
+ hour: "2-digit",
183
+ minute: "2-digit"
184
+ })
185
+ ]
186
+ }) : null] })]
187
+ }),
188
+ /* @__PURE__ */ jsx("div", { className: "w-20" })
189
+ ]
190
+ }),
191
+ /* @__PURE__ */ jsxs("div", {
192
+ className: "grid grid-cols-3 gap-4",
193
+ children: [
194
+ /* @__PURE__ */ jsxs("div", {
195
+ className: "bg-muted rounded-lg p-3 text-center",
196
+ children: [/* @__PURE__ */ jsx("p", {
197
+ className: "text-xs text-muted-foreground",
198
+ children: "Sales"
199
+ }), /* @__PURE__ */ jsx("p", {
200
+ className: "text-lg font-bold",
201
+ children: salesCount
202
+ })]
203
+ }),
204
+ /* @__PURE__ */ jsxs("div", {
205
+ className: "bg-muted rounded-lg p-3 text-center",
206
+ children: [/* @__PURE__ */ jsx("p", {
207
+ className: "text-xs text-muted-foreground",
208
+ children: "Revenue"
209
+ }), /* @__PURE__ */ jsx("p", {
210
+ className: "text-lg font-bold tabular-nums",
211
+ children: formatPaisa(salesTotal)
212
+ })]
213
+ }),
214
+ /* @__PURE__ */ jsxs("div", {
215
+ className: "bg-muted rounded-lg p-3 text-center",
216
+ children: [/* @__PURE__ */ jsx("p", {
217
+ className: "text-xs text-muted-foreground",
218
+ children: "Expected Cash"
219
+ }), /* @__PURE__ */ jsx("p", {
220
+ className: "text-lg font-bold tabular-nums",
221
+ children: formatPaisa(expectedCash)
222
+ })]
223
+ })
224
+ ]
225
+ }),
226
+ /* @__PURE__ */ jsxs("div", {
227
+ className: "space-y-3",
228
+ children: [
229
+ /* @__PURE__ */ jsxs("div", {
230
+ className: "flex items-center justify-between",
231
+ children: [/* @__PURE__ */ jsx("label", {
232
+ className: "text-sm font-medium",
233
+ children: "Actual Cash in Drawer"
234
+ }), closingCash ? /* @__PURE__ */ jsx("button", {
235
+ type: "button",
236
+ onClick: () => setClosingCash(""),
237
+ className: "text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline",
238
+ children: "Clear"
239
+ }) : null]
240
+ }),
241
+ /* @__PURE__ */ jsxs("div", {
242
+ className: "rounded-lg border bg-muted/40 p-4 text-center",
243
+ children: [/* @__PURE__ */ jsx("p", {
244
+ className: "text-xs text-muted-foreground",
245
+ children: "Counted"
246
+ }), /* @__PURE__ */ jsx("p", {
247
+ className: "text-3xl font-bold tabular-nums mt-1",
248
+ children: closingCash ? formatPaisa(actualCash) : "—"
249
+ })]
250
+ }),
251
+ /* @__PURE__ */ jsx(Numpad, {
252
+ value: closingCash,
253
+ onChange: setClosingCash,
254
+ onSubmit: isPending || !canSubmit ? void 0 : onSubmitHandler,
255
+ submitLabel: isPending ? "Closing…" : submitLabel,
256
+ density: "touch",
257
+ decimal: true
258
+ })
259
+ ]
260
+ }),
261
+ closingCash && /* @__PURE__ */ jsxs("div", {
262
+ className: `rounded-lg p-4 text-center ${difference === 0 ? "bg-green-50 dark:bg-green-950" : varianceOverThreshold ? "bg-red-50 dark:bg-red-950" : "bg-amber-50 dark:bg-amber-950"}`,
263
+ children: [/* @__PURE__ */ jsxs("div", {
264
+ className: "flex items-center justify-center gap-2 mb-1",
265
+ children: [difference === 0 ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }) : /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 text-amber-600" }), /* @__PURE__ */ jsxs("span", {
266
+ className: "text-sm font-medium",
267
+ children: [difference === 0 ? "Exact Match" : difference > 0 ? "Over" : "Short", varianceOverThreshold && " · exceeds threshold"]
268
+ })]
269
+ }), /* @__PURE__ */ jsxs("p", {
270
+ className: "text-2xl font-bold tabular-nums",
271
+ children: [difference >= 0 ? "+" : "", formatPaisa(difference)]
272
+ })]
273
+ }),
274
+ overrideRequired && !cashierIsSupervisor && !blindCloseRequired && /* @__PURE__ */ jsx("div", {
275
+ className: "rounded-lg border border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950 p-4 space-y-3",
276
+ children: !supervisorReady ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
277
+ className: "flex items-start gap-2",
278
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 mt-0.5 text-amber-700 dark:text-amber-300" }), /* @__PURE__ */ jsxs("div", {
279
+ className: "text-sm",
280
+ children: [/* @__PURE__ */ jsx("p", {
281
+ className: "font-medium text-amber-900 dark:text-amber-100",
282
+ children: "Supervisor authorization required"
283
+ }), /* @__PURE__ */ jsxs("p", {
284
+ className: "text-xs text-amber-800 dark:text-amber-200 mt-0.5",
285
+ children: [
286
+ "Variance exceeds the branch threshold",
287
+ policy ? ` (${formatPaisa(policy.varianceThresholdAbs)} or ${policy.varianceThresholdPct}%)` : "",
288
+ ". A supervisor must authenticate before closing."
289
+ ]
290
+ })]
291
+ })]
292
+ }), /* @__PURE__ */ jsxs(Button, {
293
+ variant: "default",
294
+ size: "sm",
295
+ className: "w-full bg-amber-600 hover:bg-amber-700 text-white",
296
+ onClick: () => setAuthDialogOpen(true),
297
+ disabled: isPending,
298
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 mr-2" }), "Get supervisor authorization"]
299
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
300
+ /* @__PURE__ */ jsxs("div", {
301
+ className: "flex items-start gap-2",
302
+ children: [/* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4 mt-0.5 text-green-600" }), /* @__PURE__ */ jsxs("div", {
303
+ className: "text-sm",
304
+ children: [/* @__PURE__ */ jsxs("p", {
305
+ className: "font-medium text-amber-900 dark:text-amber-100",
306
+ children: [
307
+ "Authorized by",
308
+ " ",
309
+ /* @__PURE__ */ jsx("span", {
310
+ className: "underline",
311
+ children: supervisorName ?? "Supervisor"
312
+ })
313
+ ]
314
+ }), /* @__PURE__ */ jsx("p", {
315
+ className: "text-xs text-amber-800 dark:text-amber-200 mt-0.5",
316
+ children: "Type the reason for this variance below, then close the register."
317
+ })]
318
+ })]
319
+ }),
320
+ /* @__PURE__ */ jsxs("div", {
321
+ className: "space-y-2",
322
+ children: [/* @__PURE__ */ jsx(Label, {
323
+ htmlFor: "override-reason",
324
+ children: "Override reason"
325
+ }), /* @__PURE__ */ jsx(Input, {
326
+ id: "override-reason",
327
+ value: overrideReason,
328
+ onChange: (e) => setOverrideReason(e.target.value),
329
+ placeholder: "Why is this variance acceptable?",
330
+ disabled: isPending
331
+ })]
332
+ }),
333
+ /* @__PURE__ */ jsx(Button, {
334
+ variant: "ghost",
335
+ size: "sm",
336
+ className: "text-xs",
337
+ onClick: supervisorAuth.clearAuth,
338
+ disabled: isPending,
339
+ children: "Clear authorization"
340
+ })
341
+ ] })
342
+ }),
343
+ (submitError || closeErr || blindErr) && /* @__PURE__ */ jsxs("div", {
344
+ className: "flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200",
345
+ children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError || closeErr?.message || blindErr?.message })]
346
+ }),
347
+ blindCloseRequired && /* @__PURE__ */ jsx("p", {
348
+ className: "text-xs text-center text-muted-foreground",
349
+ children: "This branch uses blind close — a manager will reconcile your counts from another terminal."
350
+ })
351
+ ]
352
+ }), /* @__PURE__ */ jsx(ManagerAuthDialog, {
353
+ open: authDialogOpen,
354
+ onOpenChange: setAuthDialogOpen,
355
+ onAuthorize: (email, password) => supervisorAuth.authorize(email, password),
356
+ isPending: supervisorAuth.isPending,
357
+ error: supervisorAuth.error,
358
+ title: "Supervisor authorization required",
359
+ description: "Variance exceeds the branch threshold. A supervisor (admin / branch_manager) must enter their credentials to authorize this close."
360
+ })]
361
+ });
362
+ }
363
+
364
+ //#endregion
365
+ export { ShiftCloseScreen };
@@ -0,0 +1,176 @@
1
+ "use client";
2
+
3
+ import { usePosAuth } from "../runtime/auth-port.js";
4
+ import { usePosRuntime } from "../runtime/config.js";
5
+ import { usePosBranch } from "../runtime/branch-port.js";
6
+ import { usePosContext } from "../state/pos-context.js";
7
+ import { formatBdt, formatPaisa } from "../lib/money.js";
8
+ import { useCallback, useMemo, useState } from "react";
9
+ import { useOpenShift, usePosCurrentShift } from "@classytic/commerce-sdk/sales";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { AlertCircle, DollarSign, LayoutDashboard, LogOut, Store } from "lucide-react";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Numpad } from "@classytic/fluid/numpad";
14
+
15
+ //#region src/screens/ShiftOpenScreen.tsx
16
+ /**
17
+ * ShiftOpenScreen — Opening cash count form.
18
+ *
19
+ * Wired to backend via `useOpenShift`. On success the POS state machine
20
+ * dispatches OPEN_SHIFT and the pos-shell fast-forwards to products.
21
+ */
22
+ function ShiftOpenScreen() {
23
+ const { dispatch } = usePosContext();
24
+ const branch = usePosBranch();
25
+ const sessionUser = usePosAuth().useSessionUser();
26
+ const { onNavigate } = usePosRuntime();
27
+ const { shift: existingShift, isLoading: isLoadingShift } = usePosCurrentShift();
28
+ const { openShift, isPending, error } = useOpenShift();
29
+ const [openingCash, setOpeningCash] = useState("");
30
+ const [submitError, setSubmitError] = useState(null);
31
+ const userName = sessionUser?.name || "Cashier";
32
+ const branchName = branch?.name || "POS Terminal";
33
+ const requiredFloat = existingShift?.policySnapshot?.requiredOpeningFloat ?? null;
34
+ const submitLabel = useMemo(() => {
35
+ if (isPending) return "Opening…";
36
+ if (requiredFloat !== null) return `Open with ${formatPaisa(requiredFloat)}`;
37
+ return "Open Register";
38
+ }, [isPending, requiredFloat]);
39
+ const handleOpenShift = useCallback(async () => {
40
+ setSubmitError(null);
41
+ const amount = Math.round((parseFloat(openingCash) || 0) * 100);
42
+ try {
43
+ await openShift({ openingCash: amount });
44
+ dispatch({ type: "OPEN_SHIFT" });
45
+ } catch (err) {
46
+ const msg = err.message || "Failed to open register";
47
+ setSubmitError(msg);
48
+ }
49
+ }, [
50
+ openingCash,
51
+ openShift,
52
+ dispatch
53
+ ]);
54
+ if (isLoadingShift) return /* @__PURE__ */ jsx("div", {
55
+ className: "flex items-center justify-center h-full",
56
+ children: /* @__PURE__ */ jsx("div", {
57
+ className: "text-muted-foreground text-sm",
58
+ children: "Loading…"
59
+ })
60
+ });
61
+ return /* @__PURE__ */ jsxs("div", {
62
+ className: "flex flex-col h-full bg-muted/30",
63
+ children: [/* @__PURE__ */ jsxs("div", {
64
+ className: "flex items-center justify-between h-12 px-4 bg-card border-b shrink-0",
65
+ children: [/* @__PURE__ */ jsxs("div", {
66
+ className: "flex items-center gap-2 min-w-0",
67
+ children: [/* @__PURE__ */ jsx(Store, { className: "h-4 w-4 text-muted-foreground shrink-0" }), /* @__PURE__ */ jsx("span", {
68
+ className: "font-semibold text-sm truncate",
69
+ children: branchName
70
+ })]
71
+ }), /* @__PURE__ */ jsxs("div", {
72
+ className: "flex items-center gap-2",
73
+ children: [/* @__PURE__ */ jsxs(Button, {
74
+ variant: "ghost",
75
+ size: "sm",
76
+ className: "h-8 text-xs gap-1",
77
+ onClick: () => {
78
+ onNavigate("/dashboard");
79
+ },
80
+ children: [/* @__PURE__ */ jsx(LayoutDashboard, { className: "h-3.5 w-3.5" }), "Dashboard"]
81
+ }), /* @__PURE__ */ jsxs(Button, {
82
+ variant: "ghost",
83
+ size: "sm",
84
+ className: "h-8 text-xs gap-1",
85
+ onClick: () => {
86
+ onNavigate("/sign-in");
87
+ },
88
+ children: [/* @__PURE__ */ jsx(LogOut, { className: "h-3.5 w-3.5" }), "Logout"]
89
+ })]
90
+ })]
91
+ }), /* @__PURE__ */ jsx("div", {
92
+ className: "flex-1 flex items-center justify-center",
93
+ children: /* @__PURE__ */ jsxs("div", {
94
+ className: "w-full max-w-md bg-card rounded-xl shadow-lg p-8 space-y-6",
95
+ children: [
96
+ /* @__PURE__ */ jsxs("div", {
97
+ className: "text-center space-y-2",
98
+ children: [
99
+ /* @__PURE__ */ jsx("div", {
100
+ className: "mx-auto w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center",
101
+ children: /* @__PURE__ */ jsx(Store, { className: "h-8 w-8 text-primary" })
102
+ }),
103
+ /* @__PURE__ */ jsx("h1", {
104
+ className: "text-2xl font-bold",
105
+ children: "Open Register"
106
+ }),
107
+ /* @__PURE__ */ jsxs("p", {
108
+ className: "text-muted-foreground text-sm",
109
+ children: [
110
+ branchName,
111
+ " · ",
112
+ userName
113
+ ]
114
+ })
115
+ ]
116
+ }),
117
+ /* @__PURE__ */ jsxs("div", {
118
+ className: "space-y-2",
119
+ children: [
120
+ /* @__PURE__ */ jsxs("label", {
121
+ className: "text-sm font-medium flex items-center gap-2",
122
+ children: [
123
+ /* @__PURE__ */ jsx(DollarSign, { className: "h-4 w-4" }),
124
+ "Starting Cash in Drawer",
125
+ requiredFloat !== null && /* @__PURE__ */ jsxs("span", {
126
+ className: "ml-auto text-xs text-muted-foreground",
127
+ children: ["Required: ", formatPaisa(requiredFloat)]
128
+ })
129
+ ]
130
+ }),
131
+ /* @__PURE__ */ jsx(Numpad, {
132
+ value: openingCash,
133
+ onChange: setOpeningCash,
134
+ onSubmit: isPending ? void 0 : handleOpenShift,
135
+ submitLabel,
136
+ quickAdd: [
137
+ 1e3,
138
+ 2e3,
139
+ 5e3,
140
+ 1e4
141
+ ],
142
+ density: "touch",
143
+ decimal: true
144
+ }),
145
+ openingCash && !submitError && /* @__PURE__ */ jsxs("p", {
146
+ className: "text-center text-sm text-muted-foreground",
147
+ children: ["Opening with ", formatBdt(parseFloat(openingCash) || 0)]
148
+ })
149
+ ]
150
+ }),
151
+ (submitError || error) && /* @__PURE__ */ jsxs("div", {
152
+ className: "flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200",
153
+ children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError || error?.message })]
154
+ }),
155
+ requiredFloat === null && /* @__PURE__ */ jsx("div", {
156
+ className: "text-center",
157
+ children: /* @__PURE__ */ jsx(Button, {
158
+ variant: "ghost",
159
+ size: "sm",
160
+ className: "text-xs text-muted-foreground",
161
+ disabled: isPending,
162
+ onClick: () => {
163
+ setOpeningCash("0");
164
+ handleOpenShift();
165
+ },
166
+ children: "Skip (start with 0)"
167
+ })
168
+ })
169
+ ]
170
+ })
171
+ })]
172
+ });
173
+ }
174
+
175
+ //#endregion
176
+ export { ShiftOpenScreen };
@@ -0,0 +1,8 @@
1
+ import { PosHardware } from "../hardware/ports.js";
2
+ import { createWebHardware } from "../hardware/web-adapter.js";
3
+ import { createTauriHardware } from "../hardware/tauri-adapter.js";
4
+ import { PosAuthPort, PosSessionUser } from "../runtime/auth-port.js";
5
+ import { PosRuntime } from "../runtime/config.js";
6
+ import { PosBranch, PosBranchPort } from "../runtime/branch-port.js";
7
+ import { PosShell, PosShellProps } from "./pos-shell.js";
8
+ export { type PosAuthPort, type PosBranch, type PosBranchPort, type PosHardware, type PosRuntime, type PosSessionUser, PosShell, type PosShellProps, createTauriHardware, createWebHardware };
@@ -0,0 +1,5 @@
1
+ import { createTauriHardware } from "../hardware/tauri-adapter.js";
2
+ import { createWebHardware } from "../hardware/web-adapter.js";
3
+ import { PosShell } from "./pos-shell.js";
4
+
5
+ export { PosShell, createTauriHardware, createWebHardware };
@@ -0,0 +1,23 @@
1
+ import { PosHardware } from "../hardware/ports.js";
2
+ import { PosAuthPort } from "../runtime/auth-port.js";
3
+ import { PosRuntime } from "../runtime/config.js";
4
+ import { PosBranchPort } from "../runtime/branch-port.js";
5
+ //#region src/shell/pos-shell.d.ts
6
+ interface PosShellProps {
7
+ /** Auth seam — host wires it to its auth client. */
8
+ auth: PosAuthPort;
9
+ /** Active-branch seam — host wires it to its branch context. */
10
+ branch: PosBranchPort;
11
+ /** Runtime services (navigation). */
12
+ runtime: PosRuntime;
13
+ /** Hardware port; defaults to the web adapter. */
14
+ hardware?: PosHardware;
15
+ }
16
+ declare function PosShell({
17
+ auth,
18
+ branch,
19
+ runtime,
20
+ hardware
21
+ }: PosShellProps): import("react").JSX.Element;
22
+ //#endregion
23
+ export { PosShell, PosShellProps };