@classytic/pos-ui 0.2.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 +112 -92
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { formatMajor, minorToMajor } from "../../lib/money.js";
|
|
4
4
|
import { cn } from "../../lib/cn.js";
|
|
5
5
|
import { useMemo, useState } from "react";
|
|
6
6
|
import { DialogWrapper } from "@classytic/fluid/client/core";
|
|
@@ -11,18 +11,60 @@ import { Button } from "@/components/ui/button";
|
|
|
11
11
|
import { Badge } from "@/components/ui/badge";
|
|
12
12
|
|
|
13
13
|
//#region src/dashboard/components/VariantSelectorDialog.tsx
|
|
14
|
+
/** Case- and space-insensitive: "Monthly" and " monthly " are the same string to a reader. */
|
|
15
|
+
const sameText = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
16
|
+
/**
|
|
17
|
+
* Should this row show `SKU: …`?
|
|
18
|
+
*
|
|
19
|
+
* Two conditions, both about whether the code carries INFORMATION for a cashier:
|
|
20
|
+
*
|
|
21
|
+
* 1. the product is stock-tracked (a SKU is an inventory identifier — a gym month
|
|
22
|
+
* and a haircut have none worth showing, same axis as the stock badge); and
|
|
23
|
+
* 2. the sku is not just a case/spacing variant of the label already displayed.
|
|
24
|
+
*/
|
|
25
|
+
function showVariantSku(product, variant) {
|
|
26
|
+
if (product?.inventory?.stockTracked === false) return false;
|
|
27
|
+
return !sameText(variant.label, variant.sku);
|
|
28
|
+
}
|
|
14
29
|
function VariantSelectorDialog({ product, open, onOpenChange, onSelect }) {
|
|
15
30
|
const [selectedSku, setSelectedSku] = useState(null);
|
|
16
31
|
const variantOptions = useMemo(() => {
|
|
17
32
|
if (!product?.variants) return [];
|
|
33
|
+
/**
|
|
34
|
+
* Is the PRODUCT counted at all? Per-product, absent ⇒ tracked.
|
|
35
|
+
*
|
|
36
|
+
* A subscription membership's "variants" are its PLANS (monthly / yearly), which hold
|
|
37
|
+
* no stock — so `getVariantStock` finds nothing, `stock > 0` is false, and every plan
|
|
38
|
+
* rendered "Out of Stock" on a product that cannot be out of stock. Same inversion the
|
|
39
|
+
* card had: absence of a quantity became a negative answer.
|
|
40
|
+
*/
|
|
41
|
+
const tracked = product.inventory?.stockTracked !== false;
|
|
18
42
|
return product.variants.filter((v) => v.isActive).map((variant) => {
|
|
19
43
|
const stock = getVariantStock(product, variant.sku);
|
|
20
|
-
const inStock = stock > 0;
|
|
44
|
+
const inStock = tracked ? stock > 0 : true;
|
|
45
|
+
/**
|
|
46
|
+
* An ABSOLUTE variant price wins over base-plus-modifier.
|
|
47
|
+
*
|
|
48
|
+
* `priceModifier` models a variant as an uplift on the product's base price, which
|
|
49
|
+
* is right for "Large: +৳50". A subscription PLAN is not an uplift — it carries its
|
|
50
|
+
* own price, and `getPosPrice` resolves the product to `plans[0]`. So the modifier
|
|
51
|
+
* form showed monthly's ৳3,000 for BOTH terms, quoting a ৳30,000 annual plan at a
|
|
52
|
+
* tenth of its price. Reading `variant.price` when present fixes the quote at the
|
|
53
|
+
* only place the cashier sees it.
|
|
54
|
+
*/
|
|
55
|
+
const absolute = variant.price;
|
|
56
|
+
const price = typeof absolute === "number" && absolute > 0 ? minorToMajor(absolute) : calculateVariantPrice(getPosPrice(product), variant.priceModifier);
|
|
21
57
|
return {
|
|
22
58
|
sku: variant.sku,
|
|
23
59
|
attributes: variant.attributes,
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
/**
|
|
61
|
+
* A NAME when the producer gave one, else the attribute-derived label, else the
|
|
62
|
+
* sku. Plans have a human label ("Monthly") and no attributes, so the
|
|
63
|
+
* attributes-only form produced an empty heading with `SKU: monthly` beneath it
|
|
64
|
+
* as the only visible text — a cashier picking a term from a raw slug.
|
|
65
|
+
*/
|
|
66
|
+
label: variant.name ?? variant.label ?? formatVariantLabel(variant.attributes) ?? variant.sku,
|
|
67
|
+
price,
|
|
26
68
|
priceModifier: variant.priceModifier || 0,
|
|
27
69
|
stock,
|
|
28
70
|
inStock,
|
|
@@ -84,7 +126,7 @@ function VariantSelectorDialog({ product, open, onOpenChange, onSelect }) {
|
|
|
84
126
|
}),
|
|
85
127
|
/* @__PURE__ */ jsxs("p", {
|
|
86
128
|
className: "text-sm text-muted-foreground",
|
|
87
|
-
children: ["Base Price: ",
|
|
129
|
+
children: ["Base Price: ", formatMajor(getPosPrice(product))]
|
|
88
130
|
}),
|
|
89
131
|
/* @__PURE__ */ jsx("div", {
|
|
90
132
|
className: "flex flex-wrap gap-1 mt-1",
|
|
@@ -114,29 +156,29 @@ function VariantSelectorDialog({ product, open, onOpenChange, onSelect }) {
|
|
|
114
156
|
className: cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", selectedSku === variant.sku ? "border-primary bg-primary" : "border-muted-foreground/30"),
|
|
115
157
|
children: selectedSku === variant.sku && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-primary-foreground" })
|
|
116
158
|
}), /* @__PURE__ */ jsxs("div", {
|
|
117
|
-
className: "text-
|
|
159
|
+
className: "text-start",
|
|
118
160
|
children: [/* @__PURE__ */ jsx("p", {
|
|
119
161
|
className: "font-medium",
|
|
120
162
|
children: variant.label
|
|
121
|
-
}), /* @__PURE__ */ jsxs("p", {
|
|
163
|
+
}), showVariantSku(product, variant) && /* @__PURE__ */ jsxs("p", {
|
|
122
164
|
className: "text-xs text-muted-foreground font-mono",
|
|
123
165
|
children: ["SKU: ", variant.sku]
|
|
124
166
|
})]
|
|
125
167
|
})]
|
|
126
168
|
}), /* @__PURE__ */ jsxs("div", {
|
|
127
169
|
className: "flex items-center gap-4",
|
|
128
|
-
children: [/* @__PURE__ */ jsx(Badge, {
|
|
170
|
+
children: [product.inventory?.stockTracked !== false && /* @__PURE__ */ jsx(Badge, {
|
|
129
171
|
variant: variant.inStock ? "secondary" : "destructive",
|
|
130
172
|
className: "text-xs",
|
|
131
173
|
children: variant.inStock ? `Stock: ${variant.stock}` : "Out of Stock"
|
|
132
174
|
}), /* @__PURE__ */ jsxs("div", {
|
|
133
|
-
className: "text-
|
|
175
|
+
className: "text-end",
|
|
134
176
|
children: [/* @__PURE__ */ jsx("p", {
|
|
135
177
|
className: "font-bold",
|
|
136
|
-
children:
|
|
178
|
+
children: formatMajor(variant.price)
|
|
137
179
|
}), variant.priceModifier !== 0 && /* @__PURE__ */ jsxs("p", {
|
|
138
180
|
className: "text-xs text-muted-foreground",
|
|
139
|
-
children: [variant.priceModifier > 0 ? "+" : "",
|
|
181
|
+
children: [variant.priceModifier > 0 ? "+" : "", formatMajor(variant.priceModifier)]
|
|
140
182
|
})]
|
|
141
183
|
})]
|
|
142
184
|
})]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { TENANT_CURRENCY_SYMBOL } from "../../../lib/money.js";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { FormDialog } from "@classytic/fluid/client/core";
|
|
5
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -33,7 +34,7 @@ function AddChargeDialog({ disabled, onAddCharge }) {
|
|
|
33
34
|
className: "w-full",
|
|
34
35
|
disabled,
|
|
35
36
|
onClick: () => setOpen(true),
|
|
36
|
-
children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4
|
|
37
|
+
children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 me-1.5" }), "Add Charge"]
|
|
37
38
|
}), /* @__PURE__ */ jsx(FormDialog, {
|
|
38
39
|
open,
|
|
39
40
|
onOpenChange: (o) => {
|
|
@@ -79,7 +80,7 @@ function AddChargeDialog({ disabled, onAddCharge }) {
|
|
|
79
80
|
className: "space-y-1.5",
|
|
80
81
|
children: [/* @__PURE__ */ jsx(Label, {
|
|
81
82
|
htmlFor: "add-charge-amount",
|
|
82
|
-
children:
|
|
83
|
+
children: `Amount (${TENANT_CURRENCY_SYMBOL})`
|
|
83
84
|
}), /* @__PURE__ */ jsx(Input, {
|
|
84
85
|
id: "add-charge-amount",
|
|
85
86
|
type: "number",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { formatMajor } from "../../../lib/money.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { ShoppingCart, Sparkles } from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
@@ -77,7 +77,7 @@ function CartItemRow({ item, onQuantityChange, onRemove }) {
|
|
|
77
77
|
/* @__PURE__ */ jsx(Button, {
|
|
78
78
|
variant: "ghost",
|
|
79
79
|
size: "sm",
|
|
80
|
-
className: "h-6
|
|
80
|
+
className: "h-6 ms-auto text-xs",
|
|
81
81
|
onClick: onRemove,
|
|
82
82
|
children: "Remove"
|
|
83
83
|
})
|
|
@@ -86,13 +86,13 @@ function CartItemRow({ item, onQuantityChange, onRemove }) {
|
|
|
86
86
|
]
|
|
87
87
|
}),
|
|
88
88
|
/* @__PURE__ */ jsxs("div", {
|
|
89
|
-
className: "text-
|
|
89
|
+
className: "text-end",
|
|
90
90
|
children: [/* @__PURE__ */ jsx("p", {
|
|
91
91
|
className: "font-bold",
|
|
92
|
-
children:
|
|
92
|
+
children: formatMajor(item.lineTotal)
|
|
93
93
|
}), /* @__PURE__ */ jsxs("p", {
|
|
94
94
|
className: "text-xs text-muted-foreground",
|
|
95
|
-
children: [
|
|
95
|
+
children: [formatMajor(item.unitPrice), " each"]
|
|
96
96
|
})]
|
|
97
97
|
})
|
|
98
98
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { formatMajor } from "../../../lib/money.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
|
|
6
6
|
//#region src/dashboard/components/cart/CartSummary.tsx
|
|
@@ -15,7 +15,7 @@ function CartSummary({ subtotal, discount, redemptionDiscount = 0, tierDiscount
|
|
|
15
15
|
children: "Subtotal"
|
|
16
16
|
}), /* @__PURE__ */ jsx("span", {
|
|
17
17
|
className: "font-medium",
|
|
18
|
-
children:
|
|
18
|
+
children: formatMajor(subtotal)
|
|
19
19
|
})]
|
|
20
20
|
}),
|
|
21
21
|
discount > 0 && /* @__PURE__ */ jsxs("div", {
|
|
@@ -25,7 +25,7 @@ function CartSummary({ subtotal, discount, redemptionDiscount = 0, tierDiscount
|
|
|
25
25
|
children: "Discount"
|
|
26
26
|
}), /* @__PURE__ */ jsxs("span", {
|
|
27
27
|
className: "font-medium",
|
|
28
|
-
children: ["-",
|
|
28
|
+
children: ["-", formatMajor(discount)]
|
|
29
29
|
})]
|
|
30
30
|
}),
|
|
31
31
|
tierDiscount > 0 && /* @__PURE__ */ jsxs("div", {
|
|
@@ -36,7 +36,7 @@ function CartSummary({ subtotal, discount, redemptionDiscount = 0, tierDiscount
|
|
|
36
36
|
}), /* @__PURE__ */ jsxs("span", {
|
|
37
37
|
className: "font-medium",
|
|
38
38
|
style: tierColor ? { color: tierColor } : void 0,
|
|
39
|
-
children: ["-",
|
|
39
|
+
children: ["-", formatMajor(tierDiscount)]
|
|
40
40
|
})]
|
|
41
41
|
}),
|
|
42
42
|
redemptionDiscount > 0 && /* @__PURE__ */ jsxs("div", {
|
|
@@ -46,12 +46,12 @@ function CartSummary({ subtotal, discount, redemptionDiscount = 0, tierDiscount
|
|
|
46
46
|
children: "Points Redeemed"
|
|
47
47
|
}), /* @__PURE__ */ jsxs("span", {
|
|
48
48
|
className: "font-medium",
|
|
49
|
-
children: ["-",
|
|
49
|
+
children: ["-", formatMajor(redemptionDiscount)]
|
|
50
50
|
})]
|
|
51
51
|
}),
|
|
52
52
|
/* @__PURE__ */ jsxs("div", {
|
|
53
53
|
className: "flex justify-between text-lg font-bold",
|
|
54
|
-
children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", { children:
|
|
54
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", { children: formatMajor(total) })]
|
|
55
55
|
}),
|
|
56
56
|
pointsToEarn > 0 && /* @__PURE__ */ jsxs("div", {
|
|
57
57
|
className: "flex justify-between text-xs text-muted-foreground",
|
|
@@ -22,7 +22,7 @@ function CustomerSection({ selected, name, phone, membershipCardId, membershipSt
|
|
|
22
22
|
size: "sm",
|
|
23
23
|
className: "h-7 px-2",
|
|
24
24
|
onClick: onClear,
|
|
25
|
-
children: [/* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5
|
|
25
|
+
children: [/* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5 me-1" }), "Clear"]
|
|
26
26
|
})]
|
|
27
27
|
}), selected ? /* @__PURE__ */ jsxs("div", {
|
|
28
28
|
className: "rounded-lg border bg-muted/30 p-3",
|
|
@@ -77,12 +77,12 @@ function CustomerSection({ selected, name, phone, membershipCardId, membershipSt
|
|
|
77
77
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
78
78
|
className: "relative flex-1",
|
|
79
79
|
children: [/* @__PURE__ */ jsx(ScanLine, {
|
|
80
|
-
className: "pointer-events-none absolute
|
|
80
|
+
className: "pointer-events-none absolute start-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground",
|
|
81
81
|
"aria-hidden": true
|
|
82
82
|
}), /* @__PURE__ */ jsx(Input, {
|
|
83
83
|
"aria-label": "Scan loyalty card",
|
|
84
84
|
placeholder: "Scan card or type ID",
|
|
85
|
-
className: "
|
|
85
|
+
className: "ps-8",
|
|
86
86
|
value: membershipCardId,
|
|
87
87
|
disabled: membershipStatus === "searching",
|
|
88
88
|
onChange: (e) => onMembershipChange(e.target.value),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { TENANT_CURRENCY_SYMBOL } from "../../../lib/money.js";
|
|
3
4
|
import { cn } from "../../../lib/cn.js";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { Lock, ShieldCheck, X } from "lucide-react";
|
|
@@ -25,14 +26,14 @@ function DiscountSection({ value, onChange, isAuthorized, authorizedByName, onRe
|
|
|
25
26
|
onClearAuth && /* @__PURE__ */ jsx("button", {
|
|
26
27
|
type: "button",
|
|
27
28
|
onClick: onClearAuth,
|
|
28
|
-
className: "
|
|
29
|
+
className: "ms-1 hover:text-red-500 transition-colors",
|
|
29
30
|
"aria-label": "Clear authorization",
|
|
30
31
|
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
31
32
|
})
|
|
32
33
|
]
|
|
33
34
|
})]
|
|
34
35
|
}), isAuthorized ? /* @__PURE__ */ jsx(Input, {
|
|
35
|
-
placeholder:
|
|
36
|
+
placeholder: `Discount (${TENANT_CURRENCY_SYMBOL})`,
|
|
36
37
|
inputMode: "numeric",
|
|
37
38
|
value,
|
|
38
39
|
onChange: (e) => onChange(e.target.value)
|
|
@@ -41,7 +42,7 @@ function DiscountSection({ value, onChange, isAuthorized, authorizedByName, onRe
|
|
|
41
42
|
variant: "outline",
|
|
42
43
|
className: cn("w-full justify-start text-muted-foreground font-normal h-10", "hover:border-amber-300 hover:bg-amber-50/50"),
|
|
43
44
|
onClick: onRequestAuth,
|
|
44
|
-
children: [/* @__PURE__ */ jsx(Lock, { className: "h-4 w-4
|
|
45
|
+
children: [/* @__PURE__ */ jsx(Lock, { className: "h-4 w-4 me-2 text-amber-500" }), "Manager authorization required"]
|
|
45
46
|
})]
|
|
46
47
|
});
|
|
47
48
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { formatMajor, formatMinor } from "../../../lib/money.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
6
|
import { Input } from "@/components/ui/input";
|
|
7
7
|
|
|
8
8
|
//#region src/dashboard/components/cart/PointsRedemptionSection.tsx
|
|
9
|
-
function PointsRedemptionSection({ enabled, pointsBalance, minRedeemPoints, maxRedeemPoints,
|
|
9
|
+
function PointsRedemptionSection({ enabled, pointsBalance, minRedeemPoints, maxRedeemPoints, pointValueMinor, estimatedDiscount, error, value, onChange, onUseMax }) {
|
|
10
10
|
if (!enabled) return null;
|
|
11
|
-
const
|
|
11
|
+
const redeemableMinor = typeof pointValueMinor === "number" && Number.isFinite(pointValueMinor) && pointValueMinor > 0 ? maxRedeemPoints * pointValueMinor : null;
|
|
12
|
+
const helperText = maxRedeemPoints > 0 ? redeemableMinor !== null ? `Up to ${maxRedeemPoints} pts (${formatMinor(redeemableMinor)})` : `Up to ${maxRedeemPoints} pts` : "Not eligible for redemption";
|
|
12
13
|
return /* @__PURE__ */ jsxs("div", {
|
|
13
14
|
className: "space-y-2",
|
|
14
15
|
children: [
|
|
@@ -44,7 +45,7 @@ function PointsRedemptionSection({ enabled, pointsBalance, minRedeemPoints, maxR
|
|
|
44
45
|
}),
|
|
45
46
|
/* @__PURE__ */ jsxs("div", {
|
|
46
47
|
className: "flex items-center justify-between text-xs text-muted-foreground",
|
|
47
|
-
children: [/* @__PURE__ */ jsx("span", { children: helperText }), estimatedDiscount > 0 && /* @__PURE__ */ jsxs("span", { children: ["-",
|
|
48
|
+
children: [/* @__PURE__ */ jsx("span", { children: helperText }), estimatedDiscount > 0 && /* @__PURE__ */ jsxs("span", { children: ["-", formatMajor(estimatedDiscount)] })]
|
|
48
49
|
}),
|
|
49
50
|
error && /* @__PURE__ */ jsx("p", {
|
|
50
51
|
className: "text-xs text-destructive",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PosCartItem } from "@classytic/commerce-sdk/sales";
|
|
2
|
+
import "@classytic/commerce-sdk/platform";
|
|
3
|
+
//#region src/dashboard/pos.types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Local POS cart item — extends the SDK's `PosCartItem` with the
|
|
6
|
+
* discriminated `kind` field used to distinguish scanned products from
|
|
7
|
+
* ad-hoc service fees (gift wrap, damaged-product reimbursement, custom
|
|
8
|
+
* handling). Survives end-to-end into the order create payload (be-prod's
|
|
9
|
+
* `PosOrderItem` schema accepts the same shape) so reports can later
|
|
10
|
+
* aggregate by `lines.kind === 'service-fee'` regardless of how the line
|
|
11
|
+
* entered the system.
|
|
12
|
+
*
|
|
13
|
+
* - `kind: 'sku'` (default, omittable) — scanned product line; uses the
|
|
14
|
+
* SDK shape verbatim (`productId`, `variantSku`, `image`, etc.).
|
|
15
|
+
* - `kind: 'service-fee'` — cashier-authored line; `productId` carries
|
|
16
|
+
* the synthetic `service:<slug>` code derived from the typed label,
|
|
17
|
+
* `productName` carries the verbatim label, `serviceCode` is the
|
|
18
|
+
* slugified discriminator the receipt + reports key on.
|
|
19
|
+
*
|
|
20
|
+
* The base type's `productId: string` stays required for both flavors so
|
|
21
|
+
* existing render paths (CartItems, receipt, parked-order resume) keep
|
|
22
|
+
* a stable identifier per row. For service fees we synthesize
|
|
23
|
+
* `service:<slug>` client-side; the backend re-derives the slug from the
|
|
24
|
+
* `label` in `pos.controller.ts`'s line builder so the persisted order
|
|
25
|
+
* line is authoritative.
|
|
26
|
+
*/
|
|
27
|
+
interface PosCartItem$1 extends PosCartItem {
|
|
28
|
+
kind?: "sku" | "service-fee";
|
|
29
|
+
serviceCode?: string;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { PosCartItem$1 as PosCartItem };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { majorToMinor } from "../lib/money.js";
|
|
2
|
+
|
|
3
|
+
//#region src/hardware/agent-adapter.ts
|
|
4
|
+
function toMinor(amount) {
|
|
5
|
+
return majorToMinor(amount ?? 0);
|
|
6
|
+
}
|
|
7
|
+
/** `ReceiptPayload` (pos-ui's major-unit UI port) → pos-agent's minor-unit wire contract. */
|
|
8
|
+
function toWireReceipt(payload) {
|
|
9
|
+
return {
|
|
10
|
+
branch: {
|
|
11
|
+
name: payload.branch.name,
|
|
12
|
+
...payload.branch.address !== void 0 ? { address: payload.branch.address } : {},
|
|
13
|
+
...payload.branch.bin !== void 0 ? { bin: payload.branch.bin } : {},
|
|
14
|
+
...payload.branch.phone !== void 0 ? { phone: payload.branch.phone } : {}
|
|
15
|
+
},
|
|
16
|
+
shift: payload.shift,
|
|
17
|
+
order: {
|
|
18
|
+
number: payload.order.number,
|
|
19
|
+
placedAt: payload.order.placedAt.toISOString(),
|
|
20
|
+
lines: payload.order.lines.map((line) => ({
|
|
21
|
+
name: line.name,
|
|
22
|
+
...line.variant !== void 0 ? { variant: line.variant } : {},
|
|
23
|
+
qty: line.qty,
|
|
24
|
+
unitPriceMinor: toMinor(line.unitPrice),
|
|
25
|
+
totalMinor: toMinor(line.total),
|
|
26
|
+
...line.taxAmount !== void 0 ? { taxAmountMinor: toMinor(line.taxAmount) } : {}
|
|
27
|
+
})),
|
|
28
|
+
subtotalMinor: toMinor(payload.order.subtotal),
|
|
29
|
+
taxMinor: toMinor(payload.order.tax),
|
|
30
|
+
...payload.order.discount !== void 0 ? { discountMinor: toMinor(payload.order.discount) } : {},
|
|
31
|
+
...payload.order.deliveryFee !== void 0 ? { deliveryFeeMinor: toMinor(payload.order.deliveryFee) } : {},
|
|
32
|
+
grandTotalMinor: toMinor(payload.order.grandTotal),
|
|
33
|
+
currency: payload.order.currency
|
|
34
|
+
},
|
|
35
|
+
payments: payload.payments.map((p) => ({
|
|
36
|
+
method: p.method,
|
|
37
|
+
amountMinor: toMinor(p.amount),
|
|
38
|
+
...p.reference !== void 0 ? { reference: p.reference } : {}
|
|
39
|
+
})),
|
|
40
|
+
...payload.changeDue !== void 0 ? { changeDueMinor: toMinor(payload.changeDue) } : {},
|
|
41
|
+
...payload.notes !== void 0 ? { notes: payload.notes } : {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function toPortOutcome(result) {
|
|
45
|
+
return result?.outcome ?? "unknown";
|
|
46
|
+
}
|
|
47
|
+
let jobCounter = 0;
|
|
48
|
+
function generateJobId(prefix) {
|
|
49
|
+
jobCounter += 1;
|
|
50
|
+
return `${prefix}_${Date.now()}_${jobCounter}`;
|
|
51
|
+
}
|
|
52
|
+
var PosAgentReceiptPrinter = class {
|
|
53
|
+
constructor(getPairing, deviceId, paperWidthMm) {
|
|
54
|
+
this.getPairing = getPairing;
|
|
55
|
+
this.deviceId = deviceId;
|
|
56
|
+
this.paperWidthMm = paperWidthMm;
|
|
57
|
+
}
|
|
58
|
+
async print(payload, copyType = "original") {
|
|
59
|
+
const pairing = this.getPairing();
|
|
60
|
+
if (!pairing) return "failed";
|
|
61
|
+
const { submitPrintJob } = await import("@classytic/pos-agent/client");
|
|
62
|
+
const request = {
|
|
63
|
+
schemaVersion: 1,
|
|
64
|
+
jobId: generateJobId(copyType === "reprint" ? "reprint" : "print"),
|
|
65
|
+
orderId: payload.order.number,
|
|
66
|
+
deviceId: this.deviceId,
|
|
67
|
+
copyType,
|
|
68
|
+
...this.paperWidthMm !== void 0 ? { paperWidthMm: this.paperWidthMm } : {},
|
|
69
|
+
codePage: "cp437",
|
|
70
|
+
receipt: toWireReceipt(payload)
|
|
71
|
+
};
|
|
72
|
+
const res = await submitPrintJob({
|
|
73
|
+
baseUrl: pairing.agentUrl,
|
|
74
|
+
localToken: pairing.localToken
|
|
75
|
+
}, request);
|
|
76
|
+
if (!res.ok) return res.timedOut ? "unknown" : "failed";
|
|
77
|
+
return toPortOutcome(res.data);
|
|
78
|
+
}
|
|
79
|
+
async isAvailable() {
|
|
80
|
+
const pairing = this.getPairing();
|
|
81
|
+
if (!pairing) return false;
|
|
82
|
+
const { getCapabilities } = await import("@classytic/pos-agent/client");
|
|
83
|
+
const res = await getCapabilities({
|
|
84
|
+
baseUrl: pairing.agentUrl,
|
|
85
|
+
localToken: pairing.localToken
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) return false;
|
|
88
|
+
return res.data.devices.find((d) => d.deviceId === this.deviceId)?.status.state === "ready";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var PosAgentCashDrawer = class {
|
|
92
|
+
constructor(getPairing, deviceId) {
|
|
93
|
+
this.getPairing = getPairing;
|
|
94
|
+
this.deviceId = deviceId;
|
|
95
|
+
}
|
|
96
|
+
async open() {
|
|
97
|
+
const pairing = this.getPairing();
|
|
98
|
+
if (!pairing) return "failed";
|
|
99
|
+
const { submitDrawerJob } = await import("@classytic/pos-agent/client");
|
|
100
|
+
const request = {
|
|
101
|
+
schemaVersion: 1,
|
|
102
|
+
jobId: generateJobId("drawer"),
|
|
103
|
+
deviceId: this.deviceId,
|
|
104
|
+
reason: "no-sale"
|
|
105
|
+
};
|
|
106
|
+
const res = await submitDrawerJob({
|
|
107
|
+
baseUrl: pairing.agentUrl,
|
|
108
|
+
localToken: pairing.localToken
|
|
109
|
+
}, request);
|
|
110
|
+
if (!res.ok) return res.timedOut ? "unknown" : "failed";
|
|
111
|
+
return toPortOutcome(res.data);
|
|
112
|
+
}
|
|
113
|
+
async isAvailable() {
|
|
114
|
+
const pairing = this.getPairing();
|
|
115
|
+
if (!pairing) return false;
|
|
116
|
+
const { getCapabilities } = await import("@classytic/pos-agent/client");
|
|
117
|
+
const res = await getCapabilities({
|
|
118
|
+
baseUrl: pairing.agentUrl,
|
|
119
|
+
localToken: pairing.localToken
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) return false;
|
|
122
|
+
return res.data.devices.find((d) => d.deviceId === this.deviceId)?.status.state === "ready";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
export { PosAgentCashDrawer, PosAgentReceiptPrinter };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/hardware/agent-pairing.d.ts
|
|
2
|
+
interface AgentPairing {
|
|
3
|
+
/** e.g. `http://127.0.0.1:9191` — resolved from pairing, never user-typed per print request. */
|
|
4
|
+
agentUrl: string;
|
|
5
|
+
deviceId: string;
|
|
6
|
+
sn: string;
|
|
7
|
+
/** The LOCAL browser↔agent token (see `@classytic/pos-agent/protocol`'s pairing module) — NOT the device-registry bearer token, which pos-agent holds and this browser never sees again after pairing. */
|
|
8
|
+
localToken: string;
|
|
9
|
+
pairedAt: string;
|
|
10
|
+
}
|
|
11
|
+
/** Synchronous read — used by `createWebHardware()` and the agent adapter on each call. */
|
|
12
|
+
declare function readAgentPairing(): AgentPairing | null;
|
|
13
|
+
declare function useAgentPairing(): {
|
|
14
|
+
pairing: AgentPairing | null;
|
|
15
|
+
setPairing: (next: AgentPairing | null) => void;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { AgentPairing, readAgentPairing, useAgentPairing };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/hardware/agent-pairing.ts
|
|
6
|
+
/**
|
|
7
|
+
* useAgentPairing — localStorage-backed pairing record for `@classytic/pos-agent`.
|
|
8
|
+
*
|
|
9
|
+
* Replaces `pos:printer:config` (raw host/port) for the agent path — per the POS
|
|
10
|
+
* hardware-bridge plan's Stream 1.2, the browser stores only the agent URL, the
|
|
11
|
+
* paired device id, and the LOCAL token; it never stores or sends a printer
|
|
12
|
+
* host/port again (device identity now lives in `@classytic/device-registry`,
|
|
13
|
+
* resolved by `pos-agent` itself at pairing time).
|
|
14
|
+
*
|
|
15
|
+
* A synchronous reader (`readAgentPairing`) exists for the same reason
|
|
16
|
+
* `readPrinterConfig` does: `createWebHardware()` in `web-adapter.ts` must decide
|
|
17
|
+
* synchronously (inside `useMemo`) whether to construct the agent adapter or the
|
|
18
|
+
* plain browser one — an async read would break that contract.
|
|
19
|
+
*/
|
|
20
|
+
const STORAGE_KEY = "pos:agent:pairing";
|
|
21
|
+
/** Synchronous read — used by `createWebHardware()` and the agent adapter on each call. */
|
|
22
|
+
function readAgentPairing() {
|
|
23
|
+
if (typeof window === "undefined") return null;
|
|
24
|
+
try {
|
|
25
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (typeof parsed.agentUrl !== "string" || typeof parsed.deviceId !== "string" || typeof parsed.localToken !== "string" || parsed.agentUrl.trim().length === 0) return null;
|
|
29
|
+
return parsed;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function writeAgentPairing(pairing) {
|
|
35
|
+
if (typeof window === "undefined") return;
|
|
36
|
+
if (!pairing) {
|
|
37
|
+
window.localStorage.removeItem(STORAGE_KEY);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(pairing));
|
|
41
|
+
}
|
|
42
|
+
function useAgentPairing() {
|
|
43
|
+
const [pairing, setPairingState] = useState(null);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setPairingState(readAgentPairing());
|
|
46
|
+
const onStorage = (e) => {
|
|
47
|
+
if (e.key !== STORAGE_KEY) return;
|
|
48
|
+
setPairingState(readAgentPairing());
|
|
49
|
+
};
|
|
50
|
+
window.addEventListener("storage", onStorage);
|
|
51
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
52
|
+
}, []);
|
|
53
|
+
return {
|
|
54
|
+
pairing,
|
|
55
|
+
setPairing: useCallback((next) => {
|
|
56
|
+
setPairingState(next);
|
|
57
|
+
writeAgentPairing(next);
|
|
58
|
+
}, [])
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { readAgentPairing, useAgentPairing };
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { PosHardware } from "./ports.js";
|
|
2
2
|
import { useBarcodeScan } from "@classytic/fluid/client/hooks";
|
|
3
|
-
|
|
4
3
|
//#region src/hardware/context.d.ts
|
|
5
4
|
interface HardwareProviderProps {
|
|
6
5
|
hardware: PosHardware;
|
|
7
6
|
children: React.ReactNode;
|
|
8
7
|
}
|
|
9
|
-
declare function HardwareProvider({
|
|
10
|
-
hardware,
|
|
11
|
-
children
|
|
12
|
-
}: HardwareProviderProps): import("react").JSX.Element;
|
|
8
|
+
declare function HardwareProvider({ hardware, children }: HardwareProviderProps): import("react").JSX.Element;
|
|
13
9
|
declare function useHardware(): PosHardware;
|
|
14
10
|
//#endregion
|
|
15
11
|
export { HardwareProvider, HardwareProviderProps, useBarcodeScan, useHardware };
|
package/dist/hardware/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { BarcodeScanner, CashDrawer, PaymentTerminal, PosHardware, ReceiptLine, ReceiptPayload, ReceiptPayment, ReceiptPrinter, ScanEvent, TerminalChargeInput, TerminalChargeResult } from "./ports.js";
|
|
2
|
-
import { RECEIPT_DOM_ID, createWebHardware } from "./web-adapter.js";
|
|
1
|
+
import { BarcodeScanner, CashDrawer, PaymentTerminal, PosHardware, PrintOutcome, ReceiptLine, ReceiptPayload, ReceiptPayment, ReceiptPrinter, ScanEvent, TerminalChargeInput, TerminalChargeResult } from "./ports.js";
|
|
2
|
+
import { RECEIPT_DOM_ID, createWebHardware, printViaBrowserWindow } from "./web-adapter.js";
|
|
3
3
|
import { createTauriHardware, isTauri } from "./tauri-adapter.js";
|
|
4
4
|
import { HardwareProvider, useBarcodeScan, useHardware } from "./context.js";
|
|
5
5
|
import { PrinterAvailability, usePrinterAvailability } from "./use-printer-availability.js";
|
|
6
6
|
import { PrinterConfig, readPrinterConfig, usePrinterConfig } from "./use-printer-config.js";
|
|
7
|
-
|
|
7
|
+
import { AgentPairing, readAgentPairing, useAgentPairing } from "./agent-pairing.js";
|
|
8
|
+
export { type AgentPairing, type BarcodeScanner, type CashDrawer, HardwareProvider, type PaymentTerminal, type PosHardware, type PrintOutcome, type PrinterAvailability, type PrinterConfig, RECEIPT_DOM_ID, type ReceiptLine, type ReceiptPayload, type ReceiptPayment, type ReceiptPrinter, type ScanEvent, type TerminalChargeInput, type TerminalChargeResult, createTauriHardware, createWebHardware, isTauri, printViaBrowserWindow, readAgentPairing, readPrinterConfig, useAgentPairing, useBarcodeScan, useHardware, usePrinterAvailability, usePrinterConfig };
|
package/dist/hardware/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readPrinterConfig, usePrinterConfig } from "./use-printer-config.js";
|
|
2
2
|
import { createTauriHardware, isTauri } from "./tauri-adapter.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readAgentPairing, useAgentPairing } from "./agent-pairing.js";
|
|
4
|
+
import { RECEIPT_DOM_ID, createWebHardware, printViaBrowserWindow } from "./web-adapter.js";
|
|
4
5
|
import { HardwareProvider, useBarcodeScan, useHardware } from "./context.js";
|
|
5
6
|
import { usePrinterAvailability } from "./use-printer-availability.js";
|
|
6
7
|
|
|
7
|
-
export { HardwareProvider, RECEIPT_DOM_ID, createTauriHardware, createWebHardware, isTauri, readPrinterConfig, useBarcodeScan, useHardware, usePrinterAvailability, usePrinterConfig };
|
|
8
|
+
export { HardwareProvider, RECEIPT_DOM_ID, createTauriHardware, createWebHardware, isTauri, printViaBrowserWindow, readAgentPairing, readPrinterConfig, useAgentPairing, useBarcodeScan, useHardware, usePrinterAvailability, usePrinterConfig };
|
package/dist/hardware/ports.d.ts
CHANGED
|
@@ -56,15 +56,32 @@ interface ReceiptPayload {
|
|
|
56
56
|
qrCodeUrl?: string;
|
|
57
57
|
notes?: string;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* `confirmed` = the adapter's part of the job completed (browser dialog opened,
|
|
61
|
+
* bytes handed to the transport). `failed` = a clear, non-ambiguous refusal
|
|
62
|
+
* (unconfigured, disallowed). `unknown` = the outcome genuinely cannot be
|
|
63
|
+
* determined (a timeout, a dropped connection after bytes were already sent) —
|
|
64
|
+
* the job may have already printed. A caller MUST NOT auto-retry `unknown`;
|
|
65
|
+
* that risks a duplicate receipt. Mirrors `@classytic/pos-agent/protocol`'s
|
|
66
|
+
* `JobOutcome` deliberately — one vocabulary for this tri-state across the
|
|
67
|
+
* whole print path, not two that could drift.
|
|
68
|
+
*/
|
|
69
|
+
type PrintOutcome = "confirmed" | "failed" | "unknown";
|
|
59
70
|
interface ReceiptPrinter {
|
|
60
|
-
/**
|
|
61
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Print a structured receipt. Adapter handles ESC/POS encoding or browser
|
|
73
|
+
* print. `copyType` defaults to `'original'`; pass `'reprint'` so an
|
|
74
|
+
* agent-backed adapter mints a NEW job id rather than deduping against the
|
|
75
|
+
* original print (see `@classytic/pos-agent`'s per-device job spool). Web
|
|
76
|
+
* and Tauri adapters ignore it — they have no job-dedupe concept.
|
|
77
|
+
*/
|
|
78
|
+
print(payload: ReceiptPayload, copyType?: "original" | "reprint"): Promise<PrintOutcome>;
|
|
62
79
|
/** Whether this adapter can talk to a printer right now (false for browser without agent). */
|
|
63
80
|
isAvailable(): Promise<boolean>;
|
|
64
81
|
}
|
|
65
82
|
interface CashDrawer {
|
|
66
83
|
/** Pulse the kick signal. Most adapters drive this through the printer's RJ11. */
|
|
67
|
-
open(): Promise<
|
|
84
|
+
open(): Promise<PrintOutcome>;
|
|
68
85
|
isAvailable(): Promise<boolean>;
|
|
69
86
|
}
|
|
70
87
|
interface ScanEvent {
|
|
@@ -113,4 +130,4 @@ interface PosHardware {
|
|
|
113
130
|
terminal: PaymentTerminal;
|
|
114
131
|
}
|
|
115
132
|
//#endregion
|
|
116
|
-
export { BarcodeScanner, CashDrawer, PaymentTerminal, PosHardware, ReceiptLine, ReceiptPayload, ReceiptPayment, ReceiptPrinter, ScanEvent, TerminalChargeInput, TerminalChargeResult };
|
|
133
|
+
export { BarcodeScanner, CashDrawer, PaymentTerminal, PosHardware, PrintOutcome, ReceiptLine, ReceiptPayload, ReceiptPayment, ReceiptPrinter, ScanEvent, TerminalChargeInput, TerminalChargeResult };
|