@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,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,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 };
|