@classytic/pos-ui 0.1.0 → 1.2.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/LICENSE +75 -0
- package/dist/components/CartSidebar.d.ts +56 -0
- package/dist/components/CartSidebar.js +10 -9
- package/dist/components/PosTopBar.d.ts +19 -0
- package/dist/components/PosTopBar.js +4 -4
- package/dist/components/PrinterSettingsDialog.d.ts +8 -0
- package/dist/components/PrinterSettingsDialog.js +506 -20
- package/dist/components/ReceiptDeliveryDialog.d.ts +11 -0
- package/dist/components/ReceiptDeliveryDialog.js +98 -0
- package/dist/components/ReceiptReprintDialog.d.ts +10 -0
- package/dist/components/ReceiptReprintDialog.js +49 -22
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +7 -0
- package/dist/dashboard/components/CustomerLookupDialog.js +108 -90
- package/dist/dashboard/components/CustomerQuickAddDialog.js +3 -1
- package/dist/dashboard/components/DeliveryPanel.js +134 -0
- package/dist/dashboard/components/ProductCard.js +17 -7
- package/dist/dashboard/components/ProductsPanel.js +4 -4
- package/dist/dashboard/components/SplitPaymentPanel.js +5 -5
- package/dist/dashboard/components/VariantSelectorDialog.js +53 -11
- package/dist/dashboard/components/cart/AddChargeDialog.js +3 -2
- package/dist/dashboard/components/cart/CartItems.js +5 -5
- package/dist/dashboard/components/cart/CartSummary.js +6 -6
- package/dist/dashboard/components/cart/CustomerSection.js +3 -3
- package/dist/dashboard/components/cart/DiscountSection.js +4 -3
- package/dist/dashboard/components/cart/PointsRedemptionSection.js +5 -4
- package/dist/dashboard/pos.types.d.ts +32 -0
- package/dist/hardware/agent-adapter.js +127 -0
- package/dist/hardware/agent-pairing.d.ts +18 -0
- package/dist/hardware/agent-pairing.js +63 -0
- package/dist/hardware/context.d.ts +1 -5
- package/dist/hardware/index.d.ts +4 -3
- package/dist/hardware/index.js +3 -2
- package/dist/hardware/ports.d.ts +21 -4
- package/dist/hardware/tauri-adapter.d.ts +0 -1
- package/dist/hardware/tauri-adapter.js +19 -5
- package/dist/hardware/web-adapter.d.ts +22 -7
- package/dist/hardware/web-adapter.js +56 -14
- package/dist/hooks/branch-scoped-key.js +64 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/sale-attempt.js +62 -0
- package/dist/hooks/useManagerAuth.js +9 -1
- package/dist/hooks/usePosCart.d.ts +55 -0
- package/dist/hooks/usePosCart.js +224 -117
- package/dist/hooks/usePosCustomer.d.ts +31 -0
- package/dist/hooks/usePosCustomer.js +32 -78
- package/dist/hooks/usePosDelivery.d.ts +28 -0
- package/dist/hooks/usePosDelivery.js +133 -0
- package/dist/hooks/usePosMultiOrder.d.ts +22 -0
- package/dist/hooks/usePosMultiOrder.js +14 -4
- package/dist/hooks/usePosPayment.d.ts +22 -0
- package/dist/lib/delivery-payload.js +83 -0
- package/dist/lib/money.js +96 -23
- package/dist/node_modules/react-hook-form/dist/index.esm.js +468 -332
- package/dist/runtime/auth-port.d.ts +1 -5
- package/dist/runtime/branch-port.d.ts +1 -5
- package/dist/runtime/config.d.ts +43 -6
- package/dist/runtime/config.js +18 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +2 -2
- package/dist/screens/OrderHistoryDrawer.d.ts +8 -0
- package/dist/screens/OrderHistoryDrawer.js +15 -15
- package/dist/screens/ParkedOrdersDrawer.d.ts +12 -0
- package/dist/screens/ParkedOrdersDrawer.js +3 -3
- package/dist/screens/PaymentScreen.d.ts +4 -0
- package/dist/screens/PaymentScreen.js +194 -33
- package/dist/screens/ProductScreen.d.ts +4 -0
- package/dist/screens/ProductScreen.js +13 -10
- package/dist/screens/ReceiptScreen.d.ts +4 -0
- package/dist/screens/ReceiptScreen.js +96 -72
- package/dist/screens/ShiftCloseScreen.d.ts +4 -0
- package/dist/screens/ShiftCloseScreen.js +18 -15
- package/dist/screens/ShiftOpenScreen.d.ts +4 -0
- package/dist/screens/ShiftOpenScreen.js +75 -19
- package/dist/screens/index.d.ts +8 -0
- package/dist/screens/index.js +9 -0
- package/dist/shell/index.d.ts +2 -0
- package/dist/shell/pos-shell.d.ts +41 -8
- package/dist/shell/pos-shell.js +25 -18
- package/dist/state/index.d.ts +3 -0
- package/dist/state/index.js +4 -0
- package/dist/state/pos-context.d.ts +13 -0
- package/dist/state/pos-state.d.ts +49 -0
- package/dist/state/pos-state.js +6 -7
- package/dist/utils/pos-helpers.js +137 -34
- package/package.json +27 -7
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { usePosBranch } from "../runtime/branch-port.js";
|
|
4
|
-
import {
|
|
4
|
+
import { formatMinor } from "../lib/money.js";
|
|
5
|
+
import { RECEIPT_DOM_ID, printViaBrowserWindow } from "../hardware/web-adapter.js";
|
|
5
6
|
import { useHardware } from "../hardware/context.js";
|
|
6
7
|
import { usePrinterAvailability } from "../hardware/use-printer-availability.js";
|
|
7
8
|
import { usePosContext } from "../state/pos-context.js";
|
|
8
|
-
import {
|
|
9
|
+
import { buildReceiptPayload } from "../utils/pos-helpers.js";
|
|
9
10
|
import { useCallback, useMemo } from "react";
|
|
10
11
|
import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
|
|
11
12
|
import { PrintableView, ResponsiveSplitLayout } from "@classytic/fluid/client/core";
|
|
13
|
+
import { DEFAULT_POS_RECEIPT_CONFIG } from "@classytic/commerce-sdk/sales";
|
|
12
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
15
|
import { CheckCircle2, Plus, Printer, ReceiptText } from "lucide-react";
|
|
14
16
|
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { useEffectiveConfig, usePlatformIdentity } from "@classytic/commerce-sdk/platform";
|
|
15
18
|
import { getReceiptLocale, paymentMethodLabel } from "@classytic/commerce-sdk/client";
|
|
16
|
-
import {
|
|
19
|
+
import { toast } from "sonner";
|
|
20
|
+
import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, DocumentParty, DocumentQR, DocumentSummary } from "@classytic/fluid/document";
|
|
21
|
+
import { renderSVG } from "uqr";
|
|
17
22
|
|
|
18
23
|
//#region src/screens/ReceiptScreen.tsx
|
|
19
24
|
/**
|
|
@@ -38,51 +43,18 @@ import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, Docume
|
|
|
38
43
|
function isShownPhone(phone) {
|
|
39
44
|
return Boolean(phone && !phone.startsWith("pending_"));
|
|
40
45
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
cashierId: "",
|
|
54
|
-
cashierName: receipt.cashier ?? ""
|
|
55
|
-
},
|
|
56
|
-
order: {
|
|
57
|
-
number: String(receipt.orderNumber ?? ""),
|
|
58
|
-
placedAt: receipt.date ? new Date(receipt.date) : /* @__PURE__ */ new Date(),
|
|
59
|
-
lines: items.map((item) => ({
|
|
60
|
-
name: item.name,
|
|
61
|
-
variant: item.variant,
|
|
62
|
-
qty: item.quantity,
|
|
63
|
-
unitPrice: item.unitPrice ?? 0,
|
|
64
|
-
total: item.total,
|
|
65
|
-
taxRate: item.vatRate,
|
|
66
|
-
taxAmount: item.vatAmount
|
|
67
|
-
})),
|
|
68
|
-
subtotal: receipt.subtotal ?? 0,
|
|
69
|
-
tax: receipt.vat?.amount ?? 0,
|
|
70
|
-
discount: receipt.discount,
|
|
71
|
-
deliveryFee: receipt.deliveryCharge,
|
|
72
|
-
grandTotal: receipt.total ?? 0,
|
|
73
|
-
currency: getReceiptLocale().currency
|
|
74
|
-
},
|
|
75
|
-
payments: receipt.payment ? [{
|
|
76
|
-
method: receipt.payment.method,
|
|
77
|
-
amount: receipt.payment.amount ?? receipt.total ?? 0,
|
|
78
|
-
reference: receipt.payment.reference
|
|
79
|
-
}] : [],
|
|
80
|
-
customer: showCustomer ? {
|
|
81
|
-
name: customerName,
|
|
82
|
-
phone: isShownPhone(customerPhoneRaw) ? customerPhoneRaw : void 0,
|
|
83
|
-
loyaltyId: receipt.membership?.cardId
|
|
84
|
-
} : void 0
|
|
85
|
-
};
|
|
46
|
+
/** Flatten a branch address to a single presentational line for the letterhead. */
|
|
47
|
+
function formatAddressLine(a) {
|
|
48
|
+
if (!a) return void 0;
|
|
49
|
+
const parts = [
|
|
50
|
+
a.line1,
|
|
51
|
+
a.line2,
|
|
52
|
+
a.city,
|
|
53
|
+
a.state,
|
|
54
|
+
a.postalCode,
|
|
55
|
+
a.country
|
|
56
|
+
].filter(Boolean);
|
|
57
|
+
return parts.length ? parts.join(", ") : void 0;
|
|
86
58
|
}
|
|
87
59
|
function buildDocumentLines(receipt) {
|
|
88
60
|
return (receipt.items ?? []).map((item, idx) => {
|
|
@@ -100,10 +72,10 @@ function buildDocumentLines(receipt) {
|
|
|
100
72
|
taxAmount: item.vatAmount ?? 0,
|
|
101
73
|
lineTotal: item.total,
|
|
102
74
|
formatted: {
|
|
103
|
-
unitPrice:
|
|
104
|
-
subtotal:
|
|
105
|
-
taxAmount:
|
|
106
|
-
lineTotal:
|
|
75
|
+
unitPrice: formatMinor(item.unitPrice ?? 0),
|
|
76
|
+
subtotal: formatMinor(subtotal),
|
|
77
|
+
taxAmount: formatMinor(item.vatAmount ?? 0),
|
|
78
|
+
lineTotal: formatMinor(item.total)
|
|
107
79
|
}
|
|
108
80
|
};
|
|
109
81
|
});
|
|
@@ -114,19 +86,47 @@ function ReceiptScreen() {
|
|
|
114
86
|
const hardware = useHardware();
|
|
115
87
|
const { available: printerAvailable, checking: printerChecking } = usePrinterAvailability();
|
|
116
88
|
const receipt = state.lastReceipt;
|
|
117
|
-
const
|
|
89
|
+
const { identity } = usePlatformIdentity();
|
|
90
|
+
const { effective: receiptCfgEff } = useEffectiveConfig("pos.receipt", { branchId: selectedBranch?.id });
|
|
91
|
+
const rcfg = receiptCfgEff?.value ?? DEFAULT_POS_RECEIPT_CONFIG;
|
|
118
92
|
const branchPhone = selectedBranch?.phone || receipt?.branch?.phone;
|
|
93
|
+
const companyLogo = identity?.company.logoUrl;
|
|
94
|
+
const companyName = identity?.branch?.name || selectedBranch?.name || receipt?.branch?.name || identity?.company.name || "Store";
|
|
95
|
+
const companyAddress = formatAddressLine(identity?.branch?.address);
|
|
96
|
+
const companyContact = identity?.branch?.phone || branchPhone || identity?.company.supportPhone;
|
|
119
97
|
const lines = useMemo(() => receipt ? buildDocumentLines(receipt) : [], [receipt]);
|
|
98
|
+
const orderRef = receipt?.orderNumber != null ? String(receipt.orderNumber) : "";
|
|
99
|
+
const qrSvg = useMemo(() => {
|
|
100
|
+
if (!rcfg.showQr || !orderRef) return null;
|
|
101
|
+
const value = rcfg.qrUrlTemplate ? rcfg.qrUrlTemplate.replace("{orderNumber}", orderRef) : orderRef;
|
|
102
|
+
try {
|
|
103
|
+
return renderSVG(value);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}, [
|
|
108
|
+
rcfg.showQr,
|
|
109
|
+
rcfg.qrUrlTemplate,
|
|
110
|
+
orderRef
|
|
111
|
+
]);
|
|
120
112
|
const handlePrint = useCallback(() => {
|
|
121
113
|
if (!receipt) return;
|
|
122
|
-
hardware.printer.print(buildReceiptPayload(receipt,
|
|
114
|
+
hardware.printer.print(buildReceiptPayload(receipt, companyName, companyContact)).then((outcome) => {
|
|
115
|
+
if (outcome === "confirmed") return;
|
|
116
|
+
toast.error(outcome === "unknown" ? "Printer status unknown — it may have already printed." : "Couldn't reach the printer.", { action: {
|
|
117
|
+
label: "Print via browser",
|
|
118
|
+
onClick: () => {
|
|
119
|
+
printViaBrowserWindow();
|
|
120
|
+
}
|
|
121
|
+
} });
|
|
122
|
+
}).catch((err) => {
|
|
123
123
|
console.warn("[pos] print failed", err);
|
|
124
124
|
});
|
|
125
125
|
}, [
|
|
126
126
|
hardware.printer,
|
|
127
127
|
receipt,
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
companyName,
|
|
129
|
+
companyContact
|
|
130
130
|
]);
|
|
131
131
|
const handleNewSale = useCallback(() => {
|
|
132
132
|
dispatch({ type: "NEW_SALE" });
|
|
@@ -149,7 +149,7 @@ function ReceiptScreen() {
|
|
|
149
149
|
}), /* @__PURE__ */ jsxs(Button, {
|
|
150
150
|
className: "mt-4",
|
|
151
151
|
onClick: handleNewSale,
|
|
152
|
-
children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4
|
|
152
|
+
children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 me-2" }), "New Sale"]
|
|
153
153
|
})]
|
|
154
154
|
})
|
|
155
155
|
});
|
|
@@ -159,7 +159,7 @@ function ReceiptScreen() {
|
|
|
159
159
|
const locale = getReceiptLocale();
|
|
160
160
|
const summaryExtra = receipt.payment ? [{
|
|
161
161
|
label: `Paid (${paymentMethodLabel(receipt.payment.method)})`,
|
|
162
|
-
value:
|
|
162
|
+
value: formatMinor(receipt.payment.amount ?? receipt.total ?? 0),
|
|
163
163
|
muted: true,
|
|
164
164
|
separator: true
|
|
165
165
|
}] : [];
|
|
@@ -197,7 +197,7 @@ function ReceiptScreen() {
|
|
|
197
197
|
}),
|
|
198
198
|
/* @__PURE__ */ jsx("p", {
|
|
199
199
|
className: "text-3xl font-bold tabular-nums mt-2",
|
|
200
|
-
children:
|
|
200
|
+
children: formatMinor(receipt.total || 0)
|
|
201
201
|
})
|
|
202
202
|
]
|
|
203
203
|
}),
|
|
@@ -209,13 +209,13 @@ function ReceiptScreen() {
|
|
|
209
209
|
onClick: handlePrint,
|
|
210
210
|
disabled: !printerAvailable && !printerChecking,
|
|
211
211
|
title: printerChecking ? "Checking printer..." : printerAvailable ? "Print receipt (Ctrl+P)" : "No printer reachable — set POS_PRINTER_HOST or check network",
|
|
212
|
-
children: [/* @__PURE__ */ jsx(Printer, { className: "h-5 w-5
|
|
212
|
+
children: [/* @__PURE__ */ jsx(Printer, { className: "h-5 w-5 me-2" }), "Print Receipt"]
|
|
213
213
|
}), /* @__PURE__ */ jsxs(Button, {
|
|
214
214
|
size: "lg",
|
|
215
215
|
variant: "outline",
|
|
216
216
|
className: "h-14 text-lg",
|
|
217
217
|
onClick: handleNewSale,
|
|
218
|
-
children: [/* @__PURE__ */ jsx(Plus, { className: "h-5 w-5
|
|
218
|
+
children: [/* @__PURE__ */ jsx(Plus, { className: "h-5 w-5 me-2" }), "New Sale (F2)"]
|
|
219
219
|
})]
|
|
220
220
|
}),
|
|
221
221
|
/* @__PURE__ */ jsx("p", {
|
|
@@ -237,13 +237,23 @@ function ReceiptScreen() {
|
|
|
237
237
|
size: "receipt",
|
|
238
238
|
children: [
|
|
239
239
|
/* @__PURE__ */ jsx(DocumentHeader, {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
...
|
|
240
|
+
...companyLogo && rcfg.showLogo ? { logo: companyLogo } : {},
|
|
241
|
+
companyName,
|
|
242
|
+
...companyAddress ? { companyAddress } : {},
|
|
243
|
+
...companyContact ? { companyContact } : {},
|
|
244
|
+
...receipt.vat?.sellerBin ? { companyTaxId: `${locale.taxIdLabel}: ${receipt.vat.sellerBin}` } : identity?.company.taxId ? { companyTaxId: `${locale.taxIdLabel}: ${identity.company.taxId}` } : {},
|
|
243
245
|
documentTitle: "Receipt",
|
|
244
246
|
documentNumber: receipt.orderNumber,
|
|
245
247
|
date: receipt.date ?? ""
|
|
246
248
|
}),
|
|
249
|
+
rcfg.headerMessage && /* @__PURE__ */ jsx("p", {
|
|
250
|
+
className: "whitespace-pre-line text-center text-xs text-muted-foreground",
|
|
251
|
+
children: rcfg.headerMessage
|
|
252
|
+
}),
|
|
253
|
+
rcfg.showCashier && receipt.cashier && /* @__PURE__ */ jsxs("p", {
|
|
254
|
+
className: "text-xs text-muted-foreground",
|
|
255
|
+
children: ["Served by: ", receipt.cashier]
|
|
256
|
+
}),
|
|
247
257
|
showCustomer && /* @__PURE__ */ jsx(DocumentParty, {
|
|
248
258
|
billTo: {
|
|
249
259
|
name: customerName,
|
|
@@ -254,7 +264,7 @@ function ReceiptScreen() {
|
|
|
254
264
|
}),
|
|
255
265
|
/* @__PURE__ */ jsx(DocumentLineTable, {
|
|
256
266
|
lines,
|
|
257
|
-
columns: [
|
|
267
|
+
columns: rcfg.basicReceipt ? ["description", "quantity"] : [
|
|
258
268
|
"description",
|
|
259
269
|
"quantity",
|
|
260
270
|
"lineTotal"
|
|
@@ -262,19 +272,33 @@ function ReceiptScreen() {
|
|
|
262
272
|
showSequence: false,
|
|
263
273
|
compact: true
|
|
264
274
|
}),
|
|
265
|
-
/* @__PURE__ */ jsx(DocumentSummary, {
|
|
266
|
-
subtotal:
|
|
267
|
-
...receipt.discount && receipt.discount > 0 ? { discount:
|
|
268
|
-
...receipt.vat?.amount && receipt.vat.amount > 0 ? { tax:
|
|
269
|
-
total:
|
|
275
|
+
!rcfg.basicReceipt && /* @__PURE__ */ jsx(DocumentSummary, {
|
|
276
|
+
subtotal: formatMinor(receipt.subtotal ?? 0),
|
|
277
|
+
...receipt.discount && receipt.discount > 0 ? { discount: formatMinor(receipt.discount) } : {},
|
|
278
|
+
...receipt.vat?.amount && receipt.vat.amount > 0 ? { tax: formatMinor(receipt.vat.amount) } : {},
|
|
279
|
+
total: formatMinor(receipt.total ?? 0),
|
|
270
280
|
extraRows: summaryExtra,
|
|
271
281
|
currency: locale.currency,
|
|
272
282
|
labels: { tax: locale.taxLabel },
|
|
273
283
|
width: "w-full"
|
|
274
284
|
}),
|
|
275
|
-
|
|
285
|
+
qrSvg && /* @__PURE__ */ jsx("div", {
|
|
286
|
+
className: "mt-3 flex justify-center",
|
|
287
|
+
children: /* @__PURE__ */ jsx(DocumentQR, {
|
|
288
|
+
size: "w-28",
|
|
289
|
+
label: rcfg.qrUrlTemplate ? "Scan for your invoice" : `Order ${orderRef}`,
|
|
290
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
291
|
+
className: "block w-full [&>svg]:h-full [&>svg]:w-full",
|
|
292
|
+
dangerouslySetInnerHTML: { __html: qrSvg }
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
}),
|
|
296
|
+
/* @__PURE__ */ jsx(DocumentFooter, { notes: /* @__PURE__ */ jsxs("div", {
|
|
276
297
|
className: "text-center",
|
|
277
|
-
children: locale.thankYouText
|
|
298
|
+
children: [/* @__PURE__ */ jsx("p", { children: locale.thankYouText }), rcfg.footerMessage && /* @__PURE__ */ jsx("p", {
|
|
299
|
+
className: "mt-1 whitespace-pre-line text-muted-foreground",
|
|
300
|
+
children: rcfg.footerMessage
|
|
301
|
+
})]
|
|
278
302
|
}) })
|
|
279
303
|
]
|
|
280
304
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { usePosAuth } from "../runtime/auth-port.js";
|
|
4
|
+
import { formatMinor, majorToMinor } from "../lib/money.js";
|
|
4
5
|
import { usePosContext } from "../state/pos-context.js";
|
|
5
|
-
import { formatPaisa } from "../lib/money.js";
|
|
6
6
|
import { useManagerAuth } from "../hooks/useManagerAuth.js";
|
|
7
7
|
import { ManagerAuthDialog } from "../dashboard/components/ManagerAuthDialog.js";
|
|
8
8
|
import { useCallback, useMemo, useState } from "react";
|
|
@@ -10,6 +10,7 @@ import { useBlindCloseShift, useCloseShift, usePosCurrentShift } from "@classyti
|
|
|
10
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { AlertCircle, AlertTriangle, ArrowLeft, CheckCircle2, ClipboardList, ShieldCheck } from "lucide-react";
|
|
12
12
|
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { presentError } from "@classytic/fluid/error-contract";
|
|
13
14
|
import { Input } from "@/components/ui/input";
|
|
14
15
|
import { Label } from "@/components/ui/label";
|
|
15
16
|
import { Numpad } from "@classytic/fluid/numpad";
|
|
@@ -62,7 +63,7 @@ function ShiftCloseScreen() {
|
|
|
62
63
|
const expectedCash = shift?.expectedCash ?? liveExpectedCash;
|
|
63
64
|
const salesCount = shift?.salesCount ?? 0;
|
|
64
65
|
const salesTotal = shift?.salesTotal ?? 0;
|
|
65
|
-
const actualCash =
|
|
66
|
+
const actualCash = majorToMinor(parseFloat(closingCash) || 0);
|
|
66
67
|
const difference = closingCash ? actualCash - expectedCash : 0;
|
|
67
68
|
const varianceAbs = Math.abs(difference);
|
|
68
69
|
const policy = shift?.policySnapshot;
|
|
@@ -124,9 +125,9 @@ function ShiftCloseScreen() {
|
|
|
124
125
|
});
|
|
125
126
|
dispatch({ type: "CONFIRM_CLOSE" });
|
|
126
127
|
} catch (err) {
|
|
127
|
-
const
|
|
128
|
-
setSubmitError(
|
|
129
|
-
if (
|
|
128
|
+
const { message } = presentError(err, "The register was not closed. Try again.");
|
|
129
|
+
setSubmitError(message);
|
|
130
|
+
if ((err instanceof Error ? err.message : "").toLowerCase().includes("variance")) setAuthDialogOpen(true);
|
|
130
131
|
}
|
|
131
132
|
}, [
|
|
132
133
|
shift,
|
|
@@ -157,6 +158,8 @@ function ShiftCloseScreen() {
|
|
|
157
158
|
supervisorReady,
|
|
158
159
|
handleSubmit
|
|
159
160
|
]);
|
|
161
|
+
const closeMutationErr = closeErr ?? blindErr;
|
|
162
|
+
const closeFailure = closeMutationErr ? presentError(closeMutationErr, "The register was not closed. Try again.") : null;
|
|
160
163
|
return /* @__PURE__ */ jsxs("div", {
|
|
161
164
|
className: "flex items-center justify-center h-full bg-muted/30",
|
|
162
165
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -169,12 +172,12 @@ function ShiftCloseScreen() {
|
|
|
169
172
|
variant: "ghost",
|
|
170
173
|
size: "sm",
|
|
171
174
|
onClick: handleCancel,
|
|
172
|
-
children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4
|
|
175
|
+
children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 me-1" }), "Cancel"]
|
|
173
176
|
}),
|
|
174
177
|
/* @__PURE__ */ jsxs("h1", {
|
|
175
178
|
className: "text-xl font-bold flex items-center gap-2",
|
|
176
179
|
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
|
|
180
|
+
className: "text-sm font-medium text-muted-foreground ms-2",
|
|
178
181
|
children: [
|
|
179
182
|
"· opened",
|
|
180
183
|
" ",
|
|
@@ -208,7 +211,7 @@ function ShiftCloseScreen() {
|
|
|
208
211
|
children: "Revenue"
|
|
209
212
|
}), /* @__PURE__ */ jsx("p", {
|
|
210
213
|
className: "text-lg font-bold tabular-nums",
|
|
211
|
-
children:
|
|
214
|
+
children: formatMinor(salesTotal)
|
|
212
215
|
})]
|
|
213
216
|
}),
|
|
214
217
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -218,7 +221,7 @@ function ShiftCloseScreen() {
|
|
|
218
221
|
children: "Expected Cash"
|
|
219
222
|
}), /* @__PURE__ */ jsx("p", {
|
|
220
223
|
className: "text-lg font-bold tabular-nums",
|
|
221
|
-
children:
|
|
224
|
+
children: formatMinor(expectedCash)
|
|
222
225
|
})]
|
|
223
226
|
})
|
|
224
227
|
]
|
|
@@ -245,7 +248,7 @@ function ShiftCloseScreen() {
|
|
|
245
248
|
children: "Counted"
|
|
246
249
|
}), /* @__PURE__ */ jsx("p", {
|
|
247
250
|
className: "text-3xl font-bold tabular-nums mt-1",
|
|
248
|
-
children: closingCash ?
|
|
251
|
+
children: closingCash ? formatMinor(actualCash) : "—"
|
|
249
252
|
})]
|
|
250
253
|
}),
|
|
251
254
|
/* @__PURE__ */ jsx(Numpad, {
|
|
@@ -268,7 +271,7 @@ function ShiftCloseScreen() {
|
|
|
268
271
|
})]
|
|
269
272
|
}), /* @__PURE__ */ jsxs("p", {
|
|
270
273
|
className: "text-2xl font-bold tabular-nums",
|
|
271
|
-
children: [difference >= 0 ? "+" : "",
|
|
274
|
+
children: [difference >= 0 ? "+" : "", formatMinor(difference)]
|
|
272
275
|
})]
|
|
273
276
|
}),
|
|
274
277
|
overrideRequired && !cashierIsSupervisor && !blindCloseRequired && /* @__PURE__ */ jsx("div", {
|
|
@@ -284,7 +287,7 @@ function ShiftCloseScreen() {
|
|
|
284
287
|
className: "text-xs text-amber-800 dark:text-amber-200 mt-0.5",
|
|
285
288
|
children: [
|
|
286
289
|
"Variance exceeds the branch threshold",
|
|
287
|
-
policy ? ` (${
|
|
290
|
+
policy ? ` (${formatMinor(policy.varianceThresholdAbs)} or ${policy.varianceThresholdPct}%)` : "",
|
|
288
291
|
". A supervisor must authenticate before closing."
|
|
289
292
|
]
|
|
290
293
|
})]
|
|
@@ -295,7 +298,7 @@ function ShiftCloseScreen() {
|
|
|
295
298
|
className: "w-full bg-amber-600 hover:bg-amber-700 text-white",
|
|
296
299
|
onClick: () => setAuthDialogOpen(true),
|
|
297
300
|
disabled: isPending,
|
|
298
|
-
children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4
|
|
301
|
+
children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 me-2" }), "Get supervisor authorization"]
|
|
299
302
|
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
300
303
|
/* @__PURE__ */ jsxs("div", {
|
|
301
304
|
className: "flex items-start gap-2",
|
|
@@ -340,9 +343,9 @@ function ShiftCloseScreen() {
|
|
|
340
343
|
})
|
|
341
344
|
] })
|
|
342
345
|
}),
|
|
343
|
-
(submitError ||
|
|
346
|
+
(submitError || closeFailure) && /* @__PURE__ */ jsxs("div", {
|
|
344
347
|
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
|
|
348
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError ?? closeFailure?.message })]
|
|
346
349
|
}),
|
|
347
350
|
blindCloseRequired && /* @__PURE__ */ jsx("p", {
|
|
348
351
|
className: "text-xs text-center text-muted-foreground",
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import { usePosAuth } from "../runtime/auth-port.js";
|
|
4
4
|
import { usePosRuntime } from "../runtime/config.js";
|
|
5
5
|
import { usePosBranch } from "../runtime/branch-port.js";
|
|
6
|
+
import { formatMajor, formatMinor, majorToMinor } from "../lib/money.js";
|
|
6
7
|
import { usePosContext } from "../state/pos-context.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { useOpenShift, usePosCurrentShift } from "@classytic/commerce-sdk/sales";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { useOpenShift, usePosCurrentShift, useRegisters } from "@classytic/commerce-sdk/sales";
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { AlertCircle, DollarSign, LayoutDashboard, LogOut, Store } from "lucide-react";
|
|
11
|
+
import { AlertCircle, DollarSign, LayoutDashboard, LogOut, Monitor, Store } from "lucide-react";
|
|
12
12
|
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { presentError } from "@classytic/fluid/error-contract";
|
|
13
14
|
import { Numpad } from "@classytic/fluid/numpad";
|
|
14
15
|
|
|
15
16
|
//#region src/screens/ShiftOpenScreen.tsx
|
|
@@ -19,6 +20,17 @@ import { Numpad } from "@classytic/fluid/numpad";
|
|
|
19
20
|
* Wired to backend via `useOpenShift`. On success the POS state machine
|
|
20
21
|
* dispatches OPEN_SHIFT and the pos-shell fast-forwards to products.
|
|
21
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The one open-shift refusal whose server message names an internal id
|
|
25
|
+
* ("An active shift already exists for scope 'ynAb…'") — verified against
|
|
26
|
+
* `ActiveShiftAlreadyOpenError` in `@classytic/pos`. Every other POS shift code
|
|
27
|
+
* carries a message a cashier can act on, so it is left to fall through.
|
|
28
|
+
*/
|
|
29
|
+
const SHIFT_OPEN_ERRORS = { ACTIVE_SHIFT_ALREADY_OPEN: {
|
|
30
|
+
title: "A register is already open",
|
|
31
|
+
message: "Close the open shift before starting a new one.",
|
|
32
|
+
retryable: false
|
|
33
|
+
} };
|
|
22
34
|
function ShiftOpenScreen() {
|
|
23
35
|
const { dispatch } = usePosContext();
|
|
24
36
|
const branch = usePosBranch();
|
|
@@ -26,32 +38,53 @@ function ShiftOpenScreen() {
|
|
|
26
38
|
const { onNavigate } = usePosRuntime();
|
|
27
39
|
const { shift: existingShift, isLoading: isLoadingShift } = usePosCurrentShift();
|
|
28
40
|
const { openShift, isPending, error } = useOpenShift();
|
|
41
|
+
const { items: registers = [], isLoading: isLoadingRegisters } = useRegisters();
|
|
29
42
|
const [openingCash, setOpeningCash] = useState("");
|
|
30
43
|
const [submitError, setSubmitError] = useState(null);
|
|
44
|
+
const [selectedRegisterId, setSelectedRegisterId] = useState(null);
|
|
45
|
+
const activeRegisters = useMemo(() => registers.filter((r) => r.isActive), [registers]);
|
|
46
|
+
const needsRegisterChoice = activeRegisters.length > 1;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (activeRegisters.length === 1 && !selectedRegisterId) setSelectedRegisterId(activeRegisters[0]?._id ?? null);
|
|
49
|
+
}, [activeRegisters, selectedRegisterId]);
|
|
50
|
+
const canOpen = !needsRegisterChoice || !!selectedRegisterId;
|
|
31
51
|
const userName = sessionUser?.name || "Cashier";
|
|
32
52
|
const branchName = branch?.name || "POS Terminal";
|
|
33
53
|
const requiredFloat = existingShift?.policySnapshot?.requiredOpeningFloat ?? null;
|
|
34
54
|
const submitLabel = useMemo(() => {
|
|
35
55
|
if (isPending) return "Opening…";
|
|
36
|
-
if (requiredFloat !== null) return `Open with ${
|
|
56
|
+
if (requiredFloat !== null) return `Open with ${formatMinor(requiredFloat)}`;
|
|
37
57
|
return "Open Register";
|
|
38
58
|
}, [isPending, requiredFloat]);
|
|
39
59
|
const handleOpenShift = useCallback(async () => {
|
|
40
60
|
setSubmitError(null);
|
|
41
|
-
|
|
61
|
+
if (needsRegisterChoice && !selectedRegisterId) {
|
|
62
|
+
setSubmitError("Select a register to open");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const amount = majorToMinor(parseFloat(openingCash) || 0);
|
|
42
66
|
try {
|
|
43
|
-
await openShift({
|
|
44
|
-
|
|
67
|
+
const openedRegisterId = (await openShift({
|
|
68
|
+
openingCash: amount,
|
|
69
|
+
...selectedRegisterId ? { registerId: selectedRegisterId } : {}
|
|
70
|
+
}))?.data?.registerId ?? selectedRegisterId ?? null;
|
|
71
|
+
dispatch({
|
|
72
|
+
type: "OPEN_SHIFT",
|
|
73
|
+
registerId: openedRegisterId
|
|
74
|
+
});
|
|
45
75
|
} catch (err) {
|
|
46
|
-
const
|
|
47
|
-
setSubmitError(
|
|
76
|
+
const { message } = presentError(err, "The register was not opened. Try again.", { codeMap: SHIFT_OPEN_ERRORS });
|
|
77
|
+
setSubmitError(message);
|
|
48
78
|
}
|
|
49
79
|
}, [
|
|
50
80
|
openingCash,
|
|
51
81
|
openShift,
|
|
52
|
-
dispatch
|
|
82
|
+
dispatch,
|
|
83
|
+
needsRegisterChoice,
|
|
84
|
+
selectedRegisterId
|
|
53
85
|
]);
|
|
54
|
-
|
|
86
|
+
const openFailure = error ? presentError(error, "The register was not opened. Try again.", { codeMap: SHIFT_OPEN_ERRORS }) : null;
|
|
87
|
+
if (isLoadingShift || isLoadingRegisters) return /* @__PURE__ */ jsx("div", {
|
|
55
88
|
className: "flex items-center justify-center h-full",
|
|
56
89
|
children: /* @__PURE__ */ jsx("div", {
|
|
57
90
|
className: "text-muted-foreground text-sm",
|
|
@@ -114,6 +147,29 @@ function ShiftOpenScreen() {
|
|
|
114
147
|
})
|
|
115
148
|
]
|
|
116
149
|
}),
|
|
150
|
+
needsRegisterChoice && /* @__PURE__ */ jsxs("div", {
|
|
151
|
+
className: "space-y-2",
|
|
152
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
153
|
+
className: "text-sm font-medium flex items-center gap-2",
|
|
154
|
+
children: [/* @__PURE__ */ jsx(Monitor, { className: "h-4 w-4" }), "Select Register"]
|
|
155
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
156
|
+
className: "grid grid-cols-2 gap-2",
|
|
157
|
+
children: activeRegisters.map((r) => /* @__PURE__ */ jsxs("button", {
|
|
158
|
+
type: "button",
|
|
159
|
+
onClick: () => {
|
|
160
|
+
setSelectedRegisterId(r._id);
|
|
161
|
+
},
|
|
162
|
+
className: `rounded-lg border p-3 text-start transition-colors ${selectedRegisterId === r._id ? "border-primary bg-primary/10 ring-1 ring-primary" : "border-border hover:bg-muted"}`,
|
|
163
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
164
|
+
className: "font-medium text-sm truncate",
|
|
165
|
+
children: r.name
|
|
166
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
167
|
+
className: "text-xs text-muted-foreground",
|
|
168
|
+
children: r.code
|
|
169
|
+
})]
|
|
170
|
+
}, r._id))
|
|
171
|
+
})]
|
|
172
|
+
}),
|
|
117
173
|
/* @__PURE__ */ jsxs("div", {
|
|
118
174
|
className: "space-y-2",
|
|
119
175
|
children: [
|
|
@@ -123,15 +179,15 @@ function ShiftOpenScreen() {
|
|
|
123
179
|
/* @__PURE__ */ jsx(DollarSign, { className: "h-4 w-4" }),
|
|
124
180
|
"Starting Cash in Drawer",
|
|
125
181
|
requiredFloat !== null && /* @__PURE__ */ jsxs("span", {
|
|
126
|
-
className: "
|
|
127
|
-
children: ["Required: ",
|
|
182
|
+
className: "ms-auto text-xs text-muted-foreground",
|
|
183
|
+
children: ["Required: ", formatMinor(requiredFloat)]
|
|
128
184
|
})
|
|
129
185
|
]
|
|
130
186
|
}),
|
|
131
187
|
/* @__PURE__ */ jsx(Numpad, {
|
|
132
188
|
value: openingCash,
|
|
133
189
|
onChange: setOpeningCash,
|
|
134
|
-
onSubmit: isPending ? void 0 : handleOpenShift,
|
|
190
|
+
onSubmit: isPending || !canOpen ? void 0 : handleOpenShift,
|
|
135
191
|
submitLabel,
|
|
136
192
|
quickAdd: [
|
|
137
193
|
1e3,
|
|
@@ -144,13 +200,13 @@ function ShiftOpenScreen() {
|
|
|
144
200
|
}),
|
|
145
201
|
openingCash && !submitError && /* @__PURE__ */ jsxs("p", {
|
|
146
202
|
className: "text-center text-sm text-muted-foreground",
|
|
147
|
-
children: ["Opening with ",
|
|
203
|
+
children: ["Opening with ", formatMajor(parseFloat(openingCash) || 0)]
|
|
148
204
|
})
|
|
149
205
|
]
|
|
150
206
|
}),
|
|
151
|
-
(submitError ||
|
|
207
|
+
(submitError || openFailure) && /* @__PURE__ */ jsxs("div", {
|
|
152
208
|
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
|
|
209
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", { children: submitError ?? openFailure?.message })]
|
|
154
210
|
}),
|
|
155
211
|
requiredFloat === null && /* @__PURE__ */ jsx("div", {
|
|
156
212
|
className: "text-center",
|
|
@@ -158,7 +214,7 @@ function ShiftOpenScreen() {
|
|
|
158
214
|
variant: "ghost",
|
|
159
215
|
size: "sm",
|
|
160
216
|
className: "text-xs text-muted-foreground",
|
|
161
|
-
disabled: isPending,
|
|
217
|
+
disabled: isPending || !canOpen,
|
|
162
218
|
onClick: () => {
|
|
163
219
|
setOpeningCash("0");
|
|
164
220
|
handleOpenShift();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { OrderHistoryDrawer } from "./OrderHistoryDrawer.js";
|
|
2
|
+
import { ParkedOrdersDrawer } from "./ParkedOrdersDrawer.js";
|
|
3
|
+
import { PaymentScreen } from "./PaymentScreen.js";
|
|
4
|
+
import { ProductScreen } from "./ProductScreen.js";
|
|
5
|
+
import { ReceiptScreen } from "./ReceiptScreen.js";
|
|
6
|
+
import { ShiftCloseScreen } from "./ShiftCloseScreen.js";
|
|
7
|
+
import { ShiftOpenScreen } from "./ShiftOpenScreen.js";
|
|
8
|
+
export { OrderHistoryDrawer, ParkedOrdersDrawer, PaymentScreen, ProductScreen, ReceiptScreen, ShiftCloseScreen, ShiftOpenScreen };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ParkedOrdersDrawer } from "./ParkedOrdersDrawer.js";
|
|
2
|
+
import { ProductScreen } from "./ProductScreen.js";
|
|
3
|
+
import { PaymentScreen } from "./PaymentScreen.js";
|
|
4
|
+
import { ReceiptScreen } from "./ReceiptScreen.js";
|
|
5
|
+
import { ShiftOpenScreen } from "./ShiftOpenScreen.js";
|
|
6
|
+
import { ShiftCloseScreen } from "./ShiftCloseScreen.js";
|
|
7
|
+
import { OrderHistoryDrawer } from "./OrderHistoryDrawer.js";
|
|
8
|
+
|
|
9
|
+
export { OrderHistoryDrawer, ParkedOrdersDrawer, PaymentScreen, ProductScreen, ReceiptScreen, ShiftCloseScreen, ShiftOpenScreen };
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { PosHardware } from "../hardware/ports.js";
|
|
2
2
|
import { createWebHardware } from "../hardware/web-adapter.js";
|
|
3
3
|
import { createTauriHardware } from "../hardware/tauri-adapter.js";
|
|
4
|
+
import "../hardware/index.js";
|
|
4
5
|
import { PosAuthPort, PosSessionUser } from "../runtime/auth-port.js";
|
|
5
6
|
import { PosRuntime } from "../runtime/config.js";
|
|
6
7
|
import { PosBranch, PosBranchPort } from "../runtime/branch-port.js";
|
|
8
|
+
import "../runtime/index.js";
|
|
7
9
|
import { PosShell, PosShellProps } from "./pos-shell.js";
|
|
8
10
|
export { type PosAuthPort, type PosBranch, type PosBranchPort, type PosHardware, type PosRuntime, type PosSessionUser, PosShell, type PosShellProps, createTauriHardware, createWebHardware };
|