@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,23 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { usePosAuth } from "../runtime/auth-port.js";
|
|
4
|
+
import { usePosSalesCapabilities } from "../runtime/config.js";
|
|
4
5
|
import { usePosBranch } from "../runtime/branch-port.js";
|
|
6
|
+
import { formatMajor } from "../lib/money.js";
|
|
5
7
|
import { usePosContext } from "../state/pos-context.js";
|
|
6
8
|
import { usePosCart } from "../hooks/usePosCart.js";
|
|
7
|
-
import { formatBdt } from "../lib/money.js";
|
|
8
9
|
import { calculateOrderTotals, isValidBDPhone, parsePositiveNumber, transformOrderToReceipt } from "../utils/pos-helpers.js";
|
|
9
10
|
import { usePosCustomer } from "../hooks/usePosCustomer.js";
|
|
11
|
+
import { deliveryBlocker, deliveryPayloadOf, isCollectOnDeliverySale, payableTotalOf } from "../lib/delivery-payload.js";
|
|
10
12
|
import { paymentNeedsReference, usePosPayment } from "../hooks/usePosPayment.js";
|
|
13
|
+
import { usePosDelivery } from "../hooks/usePosDelivery.js";
|
|
14
|
+
import { DeliveryPanel } from "../dashboard/components/DeliveryPanel.js";
|
|
11
15
|
import { SplitPaymentPanel } from "../dashboard/components/SplitPaymentPanel.js";
|
|
12
16
|
import { useCallback, useMemo, useState } from "react";
|
|
13
17
|
import { useKeyboardShortcut } from "@classytic/fluid/client/hooks";
|
|
14
18
|
import { ResponsiveSplitLayout } from "@classytic/fluid/client/core";
|
|
15
|
-
import {
|
|
19
|
+
import { usePosOrders } from "@classytic/commerce-sdk/sales";
|
|
16
20
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
21
|
import { ArrowLeft, Banknote, CreditCard, Loader2, ReceiptText } from "lucide-react";
|
|
18
22
|
import { Button } from "@/components/ui/button";
|
|
19
23
|
import { useMembershipConfig } from "@classytic/commerce-sdk/platform";
|
|
20
24
|
import { toast } from "sonner";
|
|
25
|
+
import { presentError } from "@classytic/fluid/error-contract";
|
|
21
26
|
import { Input } from "@/components/ui/input";
|
|
22
27
|
import { Numpad } from "@classytic/fluid/numpad";
|
|
23
28
|
|
|
@@ -36,11 +41,39 @@ const CASH_SUGGESTIONS = [
|
|
|
36
41
|
1e3,
|
|
37
42
|
2e3
|
|
38
43
|
];
|
|
44
|
+
/**
|
|
45
|
+
* Cashier-readable copy for the checkout refusals the till can actually cause.
|
|
46
|
+
*
|
|
47
|
+
* The server's own message is written for a developer reading a log. At a till
|
|
48
|
+
* with a queue behind it, "Tender does not settle the sale: 4500 tendered
|
|
49
|
+
* against 4700 due (underpaid by 200)" is unusable — the cashier needs the
|
|
50
|
+
* ACTION.
|
|
51
|
+
*
|
|
52
|
+
* Only two codes are listed, and both are verified against their source
|
|
53
|
+
* (`pos.tender.mismatch` in `@spinekit/pos`'s errors, `INSUFFICIENT_STOCK` in
|
|
54
|
+
* `@classytic/flow`) — a mapping for a code nothing emits is decoration that
|
|
55
|
+
* reads as coverage. Everything else falls through to the server's own message
|
|
56
|
+
* rather than being flattened into one generic string, because hiding an
|
|
57
|
+
* unexpected failure is worse than an ugly one.
|
|
58
|
+
*/
|
|
59
|
+
const CHECKOUT_ERRORS = {
|
|
60
|
+
"pos.tender.mismatch": {
|
|
61
|
+
title: "Payment doesn't match the total",
|
|
62
|
+
message: "Check the amount, or the split entries if you're splitting.",
|
|
63
|
+
retryable: false
|
|
64
|
+
},
|
|
65
|
+
INSUFFICIENT_STOCK: {
|
|
66
|
+
title: "An item is out of stock at this branch",
|
|
67
|
+
message: "Remove it from the cart, or lower the quantity.",
|
|
68
|
+
retryable: false
|
|
69
|
+
}
|
|
70
|
+
};
|
|
39
71
|
function PaymentScreen() {
|
|
40
72
|
const token = usePosAuth().getToken() || "";
|
|
41
73
|
const selectedBranch = usePosBranch();
|
|
42
|
-
const { dispatch } = usePosContext();
|
|
74
|
+
const { state, dispatch } = usePosContext();
|
|
43
75
|
const branchId = selectedBranch?.id;
|
|
76
|
+
const activeRegisterId = state.activeRegisterId;
|
|
44
77
|
const cart = usePosCart(branchId);
|
|
45
78
|
const customer = usePosCustomer(token);
|
|
46
79
|
const { membershipConfig: membership } = useMembershipConfig();
|
|
@@ -57,7 +90,31 @@ function PaymentScreen() {
|
|
|
57
90
|
customer.selectedCustomer,
|
|
58
91
|
cart.pointsToRedeemInput
|
|
59
92
|
]);
|
|
60
|
-
const
|
|
93
|
+
const sales = usePosSalesCapabilities();
|
|
94
|
+
const delivery = usePosDelivery(branchId);
|
|
95
|
+
/**
|
|
96
|
+
* WHAT THE CUSTOMER OWES — items minus discounts, PLUS the delivery charge.
|
|
97
|
+
*
|
|
98
|
+
* The server computes `dueMinor` the same way and validates the tender against it,
|
|
99
|
+
* so a screen that quoted only the cart total would collect too little and the sale
|
|
100
|
+
* would be refused at the end of checkout — after the cashier had counted cash.
|
|
101
|
+
* Every payable figure below reads this, not `orderTotals.total`: the numpad target,
|
|
102
|
+
* the split-total check, the single-payment amount, the cash suggestions and both
|
|
103
|
+
* button labels. Leaving any one of them on the cart total reintroduces the gap for
|
|
104
|
+
* exactly one code path, which is the hardest kind to notice.
|
|
105
|
+
*
|
|
106
|
+
* The cart's own total stays visible in the order summary — the customer should see
|
|
107
|
+
* goods and delivery as separate lines.
|
|
108
|
+
*/
|
|
109
|
+
const payableTotal = payableTotalOf(orderTotals.total, delivery);
|
|
110
|
+
/**
|
|
111
|
+
* Both conditions, not just the term. `usePosDelivery` already clears the term when
|
|
112
|
+
* the method flips back to carryout, so this is belt-and-braces — but it is read by
|
|
113
|
+
* the RENDER as well as by checkout, and a COD banner on a carryout sale would tell
|
|
114
|
+
* a cashier not to collect money that is owed right now.
|
|
115
|
+
*/
|
|
116
|
+
const isCollectOnDelivery = isCollectOnDeliverySale(delivery);
|
|
117
|
+
const payment = usePosPayment(token, payableTotal);
|
|
61
118
|
const { createOrder, isCreatingOrder } = usePosOrders();
|
|
62
119
|
const [cashInput, setCashInput] = useState("");
|
|
63
120
|
useKeyboardShortcut("escape", () => dispatch({ type: "GO_BACK" }), {
|
|
@@ -85,8 +142,44 @@ function PaymentScreen() {
|
|
|
85
142
|
}, [payment]);
|
|
86
143
|
const handleCheckout = useCallback(async () => {
|
|
87
144
|
if (!branchId) return;
|
|
145
|
+
/**
|
|
146
|
+
* The key for THIS sale attempt — minted once and persisted, so tapping
|
|
147
|
+
* "Complete Sale" again after a timeout resends the SAME key and the server
|
|
148
|
+
* replays the order it already created instead of ringing a second one.
|
|
149
|
+
*
|
|
150
|
+
* It is deliberately taken before any validation: an attempt that gets as
|
|
151
|
+
* far as the network must reuse whatever key the previous attempt used, and
|
|
152
|
+
* `beginSaleAttempt` only mints when there is nothing stored.
|
|
153
|
+
*/
|
|
154
|
+
const idempotencyKey = cart.beginSaleAttempt();
|
|
155
|
+
if (!idempotencyKey) {
|
|
156
|
+
toast.error("Branch not ready", { description: "The register is still resolving its branch. Try again in a moment." });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A DELIVERY sale needs a dispatchable address, and the server refuses one that
|
|
161
|
+
* is missing or unusable (`DELIVERY_ADDRESS_UNUSABLE`) before it takes a tender
|
|
162
|
+
* or holds stock. Stopping here means the cashier fixes a field instead of
|
|
163
|
+
* watching a completed-looking checkout come back as an error.
|
|
164
|
+
*/
|
|
165
|
+
const blocker = deliveryBlocker(delivery);
|
|
166
|
+
if (blocker) {
|
|
167
|
+
toast.error(blocker);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
88
170
|
let paymentPayload = {};
|
|
89
|
-
if (
|
|
171
|
+
if (isCollectOnDelivery)
|
|
172
|
+
/**
|
|
173
|
+
* COD takes NO tender — and sends none.
|
|
174
|
+
*
|
|
175
|
+
* Not an empty `payments: []` either: the server refuses a collect-on-delivery
|
|
176
|
+
* sale that also carries a payment, because the two are contradictory and only
|
|
177
|
+
* the cashier knows which was meant. Skipping the whole payment block is also
|
|
178
|
+
* what keeps the cash/reference validation below from demanding money for a
|
|
179
|
+
* sale where none changes hands.
|
|
180
|
+
*/
|
|
181
|
+
paymentPayload = {};
|
|
182
|
+
else if (payment.state.mode === "split") {
|
|
90
183
|
if (!payment.validateAllSplits()) {
|
|
91
184
|
toast.error("Please fix split payment errors");
|
|
92
185
|
return;
|
|
@@ -97,7 +190,7 @@ function PaymentScreen() {
|
|
|
97
190
|
...entry.reference.trim() && { reference: entry.reference.trim() }
|
|
98
191
|
}));
|
|
99
192
|
const splitTotal = payments.reduce((sum, p) => sum + p.amount, 0);
|
|
100
|
-
if (Math.abs(splitTotal -
|
|
193
|
+
if (Math.abs(splitTotal - payableTotal) > .01) {
|
|
101
194
|
toast.error("Split payment total must match order total");
|
|
102
195
|
return;
|
|
103
196
|
}
|
|
@@ -120,7 +213,7 @@ function PaymentScreen() {
|
|
|
120
213
|
}
|
|
121
214
|
paymentPayload = { payments: [{
|
|
122
215
|
method: payment.state.selectedMethod,
|
|
123
|
-
amount:
|
|
216
|
+
amount: payableTotal,
|
|
124
217
|
...paymentNeedsReference(payment.state.selectedMethod) && { reference: payment.state.reference.trim() }
|
|
125
218
|
}] };
|
|
126
219
|
}
|
|
@@ -141,7 +234,23 @@ function PaymentScreen() {
|
|
|
141
234
|
};
|
|
142
235
|
}),
|
|
143
236
|
branchId,
|
|
144
|
-
|
|
237
|
+
...activeRegisterId && { registerId: activeRegisterId },
|
|
238
|
+
/**
|
|
239
|
+
* The sale's own delivery + payment-term decisions, replacing a hardcoded
|
|
240
|
+
* `"pickup"` that made every till sale a carryout regardless of what the
|
|
241
|
+
* cashier had been told on the phone.
|
|
242
|
+
*
|
|
243
|
+
* `deliveryPrice` is sent only when a fee was actually entered: the server
|
|
244
|
+
* writes canonical `totals.shipping` from it, and an explicit zero would add
|
|
245
|
+
* a priced row for a delivery that costs nothing (a promotion, a loyalty
|
|
246
|
+
* perk) rather than leaving the sale byte-identical to a carryout.
|
|
247
|
+
*
|
|
248
|
+
* `paymentTerm` is sent ALWAYS, prepaid included. The till knows the answer,
|
|
249
|
+
* and a stated fact beats one every downstream reader has to infer from
|
|
250
|
+
* absence — absence is also what an order written before the field existed
|
|
251
|
+
* looks like, and those are different claims.
|
|
252
|
+
*/
|
|
253
|
+
...deliveryPayloadOf(delivery),
|
|
145
254
|
...cart.membershipCardId.trim() && { membershipCardId: cart.membershipCardId.trim() },
|
|
146
255
|
customer: {
|
|
147
256
|
name: customer.customerName.trim() || "Walk-in Customer",
|
|
@@ -151,30 +260,50 @@ function PaymentScreen() {
|
|
|
151
260
|
...paymentPayload,
|
|
152
261
|
...orderTotals.manualDiscount > 0 && { discount: orderTotals.manualDiscount },
|
|
153
262
|
...orderTotals.pointsRedeemed > 0 && { pointsToRedeem: orderTotals.pointsRedeemed },
|
|
154
|
-
idempotencyKey
|
|
263
|
+
idempotencyKey
|
|
155
264
|
};
|
|
156
|
-
const
|
|
265
|
+
const response = await createOrder(orderData);
|
|
266
|
+
const branchInfo = {
|
|
157
267
|
name: selectedBranch?.name || "Store",
|
|
158
268
|
phone: selectedBranch?.phone
|
|
159
|
-
}
|
|
269
|
+
};
|
|
270
|
+
const receipt = transformOrderToReceipt(response, branchInfo);
|
|
160
271
|
cart.resetCart();
|
|
161
272
|
payment.reset();
|
|
162
273
|
customer.resetCustomer();
|
|
274
|
+
/**
|
|
275
|
+
* Delivery too — it is PERSISTED (branch-scoped localStorage), so unlike
|
|
276
|
+
* per-instance state it survives the sale unless cleared.
|
|
277
|
+
*
|
|
278
|
+
* Leaving it would carry one customer's address, phone and delivery charge
|
|
279
|
+
* onto the next customer's sale. That fails in the worst possible direction:
|
|
280
|
+
* the next order looks complete, passes validation, and dispatches goods to
|
|
281
|
+
* the wrong person — and the sale it was copied from was correct, so nothing
|
|
282
|
+
* about either order reads as an error.
|
|
283
|
+
*/
|
|
284
|
+
delivery.reset();
|
|
163
285
|
dispatch({
|
|
164
286
|
type: "COMPLETE_SALE",
|
|
165
287
|
receipt
|
|
166
288
|
});
|
|
167
289
|
toast.success("Order completed!");
|
|
168
290
|
} catch (error) {
|
|
169
|
-
const message = error
|
|
170
|
-
|
|
291
|
+
const { title, message } = presentError(error, "The sale was not completed. Try again.", {
|
|
292
|
+
codeMap: CHECKOUT_ERRORS,
|
|
293
|
+
fallbackTitle: "Could not complete the sale"
|
|
294
|
+
});
|
|
295
|
+
toast.error(title, { description: message });
|
|
171
296
|
}
|
|
172
297
|
}, [
|
|
173
298
|
branchId,
|
|
299
|
+
activeRegisterId,
|
|
174
300
|
selectedBranch,
|
|
175
301
|
cart,
|
|
176
302
|
customer,
|
|
177
303
|
payment,
|
|
304
|
+
delivery,
|
|
305
|
+
isCollectOnDelivery,
|
|
306
|
+
payableTotal,
|
|
178
307
|
orderTotals,
|
|
179
308
|
createOrder,
|
|
180
309
|
dispatch
|
|
@@ -199,7 +328,7 @@ function PaymentScreen() {
|
|
|
199
328
|
variant: "ghost",
|
|
200
329
|
size: "sm",
|
|
201
330
|
onClick: () => dispatch({ type: "GO_BACK" }),
|
|
202
|
-
children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4
|
|
331
|
+
children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4 me-1" }), "Back"]
|
|
203
332
|
}), /* @__PURE__ */ jsx("h2", {
|
|
204
333
|
className: "font-semibold text-lg",
|
|
205
334
|
children: "Order Summary"
|
|
@@ -217,7 +346,7 @@ function PaymentScreen() {
|
|
|
217
346
|
children: item.productName
|
|
218
347
|
}),
|
|
219
348
|
item.variantLabel && /* @__PURE__ */ jsxs("span", {
|
|
220
|
-
className: "text-muted-foreground
|
|
349
|
+
className: "text-muted-foreground ms-1",
|
|
221
350
|
children: [
|
|
222
351
|
"(",
|
|
223
352
|
item.variantLabel,
|
|
@@ -225,13 +354,13 @@ function PaymentScreen() {
|
|
|
225
354
|
]
|
|
226
355
|
}),
|
|
227
356
|
/* @__PURE__ */ jsxs("span", {
|
|
228
|
-
className: "text-muted-foreground
|
|
357
|
+
className: "text-muted-foreground ms-2",
|
|
229
358
|
children: ["x", item.quantity]
|
|
230
359
|
})
|
|
231
360
|
]
|
|
232
361
|
}), /* @__PURE__ */ jsx("span", {
|
|
233
|
-
className: "font-medium tabular-nums shrink-0
|
|
234
|
-
children:
|
|
362
|
+
className: "font-medium tabular-nums shrink-0 ms-4",
|
|
363
|
+
children: formatMajor(item.lineTotal)
|
|
235
364
|
})]
|
|
236
365
|
}, `${item.productId}-${item.variantSku || ""}`))
|
|
237
366
|
}),
|
|
@@ -242,14 +371,14 @@ function PaymentScreen() {
|
|
|
242
371
|
className: "flex justify-between text-sm",
|
|
243
372
|
children: [/* @__PURE__ */ jsx("span", { children: "Subtotal" }), /* @__PURE__ */ jsx("span", {
|
|
244
373
|
className: "tabular-nums",
|
|
245
|
-
children:
|
|
374
|
+
children: formatMajor(orderTotals.subtotal)
|
|
246
375
|
})]
|
|
247
376
|
}),
|
|
248
377
|
orderTotals.manualDiscount > 0 && /* @__PURE__ */ jsxs("div", {
|
|
249
378
|
className: "flex justify-between text-sm text-destructive",
|
|
250
379
|
children: [/* @__PURE__ */ jsx("span", { children: "Discount" }), /* @__PURE__ */ jsxs("span", {
|
|
251
380
|
className: "tabular-nums",
|
|
252
|
-
children: ["-",
|
|
381
|
+
children: ["-", formatMajor(orderTotals.manualDiscount)]
|
|
253
382
|
})]
|
|
254
383
|
}),
|
|
255
384
|
orderTotals.tierDiscount > 0 && /* @__PURE__ */ jsxs("div", {
|
|
@@ -260,14 +389,28 @@ function PaymentScreen() {
|
|
|
260
389
|
")"
|
|
261
390
|
] }), /* @__PURE__ */ jsxs("span", {
|
|
262
391
|
className: "tabular-nums",
|
|
263
|
-
children: ["-",
|
|
392
|
+
children: ["-", formatMajor(orderTotals.tierDiscount)]
|
|
393
|
+
})]
|
|
394
|
+
}),
|
|
395
|
+
delivery.fee > 0 && /* @__PURE__ */ jsxs("div", {
|
|
396
|
+
className: "flex justify-between text-sm",
|
|
397
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Delivery" }), /* @__PURE__ */ jsx("span", {
|
|
398
|
+
className: "tabular-nums",
|
|
399
|
+
children: formatMajor(delivery.fee)
|
|
264
400
|
})]
|
|
265
401
|
}),
|
|
266
402
|
/* @__PURE__ */ jsxs("div", {
|
|
267
403
|
className: "flex justify-between text-lg font-bold pt-2 border-t",
|
|
268
404
|
children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", {
|
|
269
405
|
className: "tabular-nums",
|
|
270
|
-
children:
|
|
406
|
+
children: formatMajor(payableTotal)
|
|
407
|
+
})]
|
|
408
|
+
}),
|
|
409
|
+
isCollectOnDelivery && /* @__PURE__ */ jsxs("div", {
|
|
410
|
+
className: "flex justify-between text-sm font-medium text-amber-600",
|
|
411
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Collect on delivery" }), /* @__PURE__ */ jsx("span", {
|
|
412
|
+
className: "tabular-nums",
|
|
413
|
+
children: formatMajor(payableTotal)
|
|
271
414
|
})]
|
|
272
415
|
})
|
|
273
416
|
]
|
|
@@ -291,16 +434,21 @@ function PaymentScreen() {
|
|
|
291
434
|
/* @__PURE__ */ jsxs("div", {
|
|
292
435
|
className: "flex-1 overflow-y-auto px-6 py-4 space-y-4",
|
|
293
436
|
children: [
|
|
294
|
-
|
|
437
|
+
/* @__PURE__ */ jsx(DeliveryPanel, {
|
|
438
|
+
delivery,
|
|
439
|
+
collectOnDeliveryEnabled: sales.collectOnDelivery === true,
|
|
440
|
+
disabled: isCreatingOrder
|
|
441
|
+
}),
|
|
442
|
+
!isCollectOnDelivery && payment.state.mode !== "split" && /* @__PURE__ */ jsx("div", {
|
|
295
443
|
className: "grid grid-cols-2 gap-2",
|
|
296
444
|
children: payment.options.map((opt) => /* @__PURE__ */ jsxs(Button, {
|
|
297
445
|
variant: payment.state.selectedKey === opt.key ? "default" : "outline",
|
|
298
446
|
className: "h-12 text-sm",
|
|
299
447
|
onClick: () => payment.selectPayment(opt.key),
|
|
300
|
-
children: [opt.key === "cash" ? /* @__PURE__ */ jsx(Banknote, { className: "h-4 w-4
|
|
448
|
+
children: [opt.key === "cash" ? /* @__PURE__ */ jsx(Banknote, { className: "h-4 w-4 me-2" }) : /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4 me-2" }), opt.label]
|
|
301
449
|
}, opt.key))
|
|
302
450
|
}),
|
|
303
|
-
/* @__PURE__ */ jsxs("div", {
|
|
451
|
+
!isCollectOnDelivery && /* @__PURE__ */ jsxs("div", {
|
|
304
452
|
className: "flex gap-2",
|
|
305
453
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
306
454
|
variant: payment.state.mode === "single" ? "default" : "outline",
|
|
@@ -314,10 +462,23 @@ function PaymentScreen() {
|
|
|
314
462
|
children: "Split (F8)"
|
|
315
463
|
})]
|
|
316
464
|
}),
|
|
317
|
-
|
|
465
|
+
isCollectOnDelivery ? /* @__PURE__ */ jsxs("div", {
|
|
466
|
+
className: "rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm dark:border-amber-800 dark:bg-amber-950",
|
|
467
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
468
|
+
className: "font-medium",
|
|
469
|
+
children: "Cash on delivery"
|
|
470
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
471
|
+
className: "mt-1 text-muted-foreground",
|
|
472
|
+
children: [
|
|
473
|
+
"Nothing is collected at the till. The order records ",
|
|
474
|
+
formatMajor(payableTotal),
|
|
475
|
+
" as owed, and the courier collects it on delivery."
|
|
476
|
+
]
|
|
477
|
+
})]
|
|
478
|
+
}) : payment.state.mode === "split" ? /* @__PURE__ */ jsx(SplitPaymentPanel, {
|
|
318
479
|
entries: payment.state.splitEntries,
|
|
319
480
|
options: payment.options,
|
|
320
|
-
total:
|
|
481
|
+
total: payableTotal,
|
|
321
482
|
remaining: payment.state.splitRemaining,
|
|
322
483
|
onAdd: payment.addSplit,
|
|
323
484
|
onUpdate: payment.updateSplit,
|
|
@@ -328,9 +489,9 @@ function PaymentScreen() {
|
|
|
328
489
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
329
490
|
variant: "outline",
|
|
330
491
|
size: "sm",
|
|
331
|
-
onClick: () => handleCashSuggestion(Math.ceil(
|
|
492
|
+
onClick: () => handleCashSuggestion(Math.ceil(payableTotal)),
|
|
332
493
|
children: "Exact"
|
|
333
|
-
}), CASH_SUGGESTIONS.filter((s) => s >=
|
|
494
|
+
}), CASH_SUGGESTIONS.filter((s) => s >= payableTotal).slice(0, 5).map((amount) => /* @__PURE__ */ jsx(Button, {
|
|
334
495
|
variant: "outline",
|
|
335
496
|
size: "sm",
|
|
336
497
|
onClick: () => handleCashSuggestion(amount),
|
|
@@ -344,7 +505,7 @@ function PaymentScreen() {
|
|
|
344
505
|
payment.setCashReceived(v);
|
|
345
506
|
},
|
|
346
507
|
onSubmit: isCreatingOrder ? void 0 : handleCheckout,
|
|
347
|
-
submitLabel: `Pay ${
|
|
508
|
+
submitLabel: `Pay ${formatMajor(payableTotal)}`,
|
|
348
509
|
quickAdd: [
|
|
349
510
|
100,
|
|
350
511
|
200,
|
|
@@ -361,7 +522,7 @@ function PaymentScreen() {
|
|
|
361
522
|
children: "Change"
|
|
362
523
|
}), /* @__PURE__ */ jsx("p", {
|
|
363
524
|
className: "text-2xl font-bold text-green-600 tabular-nums",
|
|
364
|
-
children:
|
|
525
|
+
children: formatMajor(payment.state.changeAmount)
|
|
365
526
|
})]
|
|
366
527
|
})
|
|
367
528
|
] }) : /* @__PURE__ */ jsxs("div", {
|
|
@@ -378,13 +539,13 @@ function PaymentScreen() {
|
|
|
378
539
|
})
|
|
379
540
|
]
|
|
380
541
|
}),
|
|
381
|
-
(payment.state.mode === "split" || !isCash) && /* @__PURE__ */ jsx("div", {
|
|
542
|
+
(isCollectOnDelivery || payment.state.mode === "split" || !isCash) && /* @__PURE__ */ jsx("div", {
|
|
382
543
|
className: "px-6 py-4 border-t",
|
|
383
544
|
children: /* @__PURE__ */ jsx(Button, {
|
|
384
545
|
className: "w-full h-12 text-base font-semibold",
|
|
385
546
|
onClick: handleCheckout,
|
|
386
547
|
disabled: isCreatingOrder,
|
|
387
|
-
children: isCreatingOrder ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5
|
|
548
|
+
children: isCreatingOrder ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 me-2 animate-spin" }), "Processing..."] }) : isCollectOnDelivery ? `Place COD order ${formatMajor(payableTotal)}` : `Complete Sale ${formatMajor(payableTotal)}`
|
|
388
549
|
})
|
|
389
550
|
})
|
|
390
551
|
]
|
|
@@ -50,6 +50,12 @@ function ProductScreen() {
|
|
|
50
50
|
const [discountAuthDialogOpen, setDiscountAuthDialogOpen] = useState(false);
|
|
51
51
|
const cart = usePosCart(branchId);
|
|
52
52
|
const customer = usePosCustomer(token);
|
|
53
|
+
/**
|
|
54
|
+
* What the cashier typed in the lookup dialog, handed over when they press
|
|
55
|
+
* "Add new". Held here only because two sibling dialogs need it — the lookup
|
|
56
|
+
* owns its own search state, so this is the ONE value that has to cross.
|
|
57
|
+
*/
|
|
58
|
+
const [quickAddPrefill, setQuickAddPrefill] = useState({});
|
|
53
59
|
const multiOrder = usePosMultiOrder(branchId);
|
|
54
60
|
const [parkedDrawerOpen, setParkedDrawerOpen] = useState(false);
|
|
55
61
|
const { membershipConfig: membership } = useMembershipConfig();
|
|
@@ -67,12 +73,13 @@ function ProductScreen() {
|
|
|
67
73
|
customer.selectedCustomer,
|
|
68
74
|
cart.pointsToRedeemInput
|
|
69
75
|
]);
|
|
70
|
-
const
|
|
76
|
+
const productFilters = useMemo(() => ({
|
|
71
77
|
search: appliedSearch.trim() || void 0,
|
|
72
78
|
category: selectedCategory !== "all" ? selectedCategory : void 0,
|
|
73
79
|
inStockOnly: true,
|
|
74
80
|
limit: 50
|
|
75
|
-
}), [appliedSearch, selectedCategory])
|
|
81
|
+
}), [appliedSearch, selectedCategory]);
|
|
82
|
+
const { products, isLoading: productsLoading, isFetching, refetch } = usePosProducts(branchId, productFilters, { enabled: !!branchId });
|
|
76
83
|
const lookupMutation = usePosLookupMutation();
|
|
77
84
|
const { items: categoryTree = [] } = useCategoryTree();
|
|
78
85
|
const categories = useMemo(() => categoryTree.map((cat) => ({
|
|
@@ -283,13 +290,9 @@ function ProductScreen() {
|
|
|
283
290
|
/* @__PURE__ */ jsx(CustomerLookupDialog, {
|
|
284
291
|
open: customer.lookupDialogOpen,
|
|
285
292
|
onOpenChange: customer.setLookupDialogOpen,
|
|
286
|
-
searchValue: customer.customerSearch,
|
|
287
|
-
onSearchValueChange: customer.setCustomerSearch,
|
|
288
|
-
onSearch: customer.triggerSearch,
|
|
289
|
-
results: customer.customerResults,
|
|
290
|
-
isLoading: customer.isSearching,
|
|
291
293
|
onSelect: customer.selectCustomer,
|
|
292
|
-
onCreate: () => {
|
|
294
|
+
onCreate: (prefill) => {
|
|
295
|
+
setQuickAddPrefill(prefill);
|
|
293
296
|
customer.setLookupDialogOpen(false);
|
|
294
297
|
customer.setCreateDialogOpen(true);
|
|
295
298
|
}
|
|
@@ -298,8 +301,8 @@ function ProductScreen() {
|
|
|
298
301
|
token,
|
|
299
302
|
open: customer.createDialogOpen,
|
|
300
303
|
onOpenChange: customer.setCreateDialogOpen,
|
|
301
|
-
defaultName: customer.customerName,
|
|
302
|
-
defaultPhone: isValidPhone(
|
|
304
|
+
defaultName: quickAddPrefill.name || customer.customerName,
|
|
305
|
+
defaultPhone: quickAddPrefill.phone && isValidPhone(quickAddPrefill.phone) ? quickAddPrefill.phone : customer.customerPhone,
|
|
303
306
|
onCreated: customer.handleCustomerCreated
|
|
304
307
|
}),
|
|
305
308
|
/* @__PURE__ */ jsx(ManagerAuthDialog, {
|