@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.
Files changed (87) hide show
  1. package/LICENSE +75 -0
  2. package/dist/components/CartSidebar.d.ts +56 -0
  3. package/dist/components/CartSidebar.js +10 -9
  4. package/dist/components/PosTopBar.d.ts +19 -0
  5. package/dist/components/PosTopBar.js +4 -4
  6. package/dist/components/PrinterSettingsDialog.d.ts +8 -0
  7. package/dist/components/PrinterSettingsDialog.js +506 -20
  8. package/dist/components/ReceiptDeliveryDialog.d.ts +11 -0
  9. package/dist/components/ReceiptDeliveryDialog.js +98 -0
  10. package/dist/components/ReceiptReprintDialog.d.ts +10 -0
  11. package/dist/components/ReceiptReprintDialog.js +49 -22
  12. package/dist/components/index.d.ts +6 -0
  13. package/dist/components/index.js +7 -0
  14. package/dist/dashboard/components/CustomerLookupDialog.js +108 -90
  15. package/dist/dashboard/components/CustomerQuickAddDialog.js +3 -1
  16. package/dist/dashboard/components/DeliveryPanel.js +134 -0
  17. package/dist/dashboard/components/ProductCard.js +17 -7
  18. package/dist/dashboard/components/ProductsPanel.js +4 -4
  19. package/dist/dashboard/components/SplitPaymentPanel.js +5 -5
  20. package/dist/dashboard/components/VariantSelectorDialog.js +53 -11
  21. package/dist/dashboard/components/cart/AddChargeDialog.js +3 -2
  22. package/dist/dashboard/components/cart/CartItems.js +5 -5
  23. package/dist/dashboard/components/cart/CartSummary.js +6 -6
  24. package/dist/dashboard/components/cart/CustomerSection.js +3 -3
  25. package/dist/dashboard/components/cart/DiscountSection.js +4 -3
  26. package/dist/dashboard/components/cart/PointsRedemptionSection.js +5 -4
  27. package/dist/dashboard/pos.types.d.ts +32 -0
  28. package/dist/hardware/agent-adapter.js +127 -0
  29. package/dist/hardware/agent-pairing.d.ts +18 -0
  30. package/dist/hardware/agent-pairing.js +63 -0
  31. package/dist/hardware/context.d.ts +1 -5
  32. package/dist/hardware/index.d.ts +4 -3
  33. package/dist/hardware/index.js +3 -2
  34. package/dist/hardware/ports.d.ts +21 -4
  35. package/dist/hardware/tauri-adapter.d.ts +0 -1
  36. package/dist/hardware/tauri-adapter.js +19 -5
  37. package/dist/hardware/web-adapter.d.ts +22 -7
  38. package/dist/hardware/web-adapter.js +56 -14
  39. package/dist/hooks/branch-scoped-key.js +64 -0
  40. package/dist/hooks/index.d.ts +6 -0
  41. package/dist/hooks/index.js +7 -0
  42. package/dist/hooks/sale-attempt.js +62 -0
  43. package/dist/hooks/useManagerAuth.js +9 -1
  44. package/dist/hooks/usePosCart.d.ts +55 -0
  45. package/dist/hooks/usePosCart.js +224 -117
  46. package/dist/hooks/usePosCustomer.d.ts +31 -0
  47. package/dist/hooks/usePosCustomer.js +32 -78
  48. package/dist/hooks/usePosDelivery.d.ts +28 -0
  49. package/dist/hooks/usePosDelivery.js +133 -0
  50. package/dist/hooks/usePosMultiOrder.d.ts +22 -0
  51. package/dist/hooks/usePosMultiOrder.js +14 -4
  52. package/dist/hooks/usePosPayment.d.ts +22 -0
  53. package/dist/lib/delivery-payload.js +83 -0
  54. package/dist/lib/money.js +96 -23
  55. package/dist/node_modules/react-hook-form/dist/index.esm.js +468 -332
  56. package/dist/runtime/auth-port.d.ts +1 -5
  57. package/dist/runtime/branch-port.d.ts +1 -5
  58. package/dist/runtime/config.d.ts +43 -6
  59. package/dist/runtime/config.js +18 -1
  60. package/dist/runtime/index.d.ts +2 -2
  61. package/dist/runtime/index.js +2 -2
  62. package/dist/screens/OrderHistoryDrawer.d.ts +8 -0
  63. package/dist/screens/OrderHistoryDrawer.js +15 -15
  64. package/dist/screens/ParkedOrdersDrawer.d.ts +12 -0
  65. package/dist/screens/ParkedOrdersDrawer.js +3 -3
  66. package/dist/screens/PaymentScreen.d.ts +4 -0
  67. package/dist/screens/PaymentScreen.js +194 -33
  68. package/dist/screens/ProductScreen.d.ts +4 -0
  69. package/dist/screens/ProductScreen.js +13 -10
  70. package/dist/screens/ReceiptScreen.d.ts +4 -0
  71. package/dist/screens/ReceiptScreen.js +96 -72
  72. package/dist/screens/ShiftCloseScreen.d.ts +4 -0
  73. package/dist/screens/ShiftCloseScreen.js +18 -15
  74. package/dist/screens/ShiftOpenScreen.d.ts +4 -0
  75. package/dist/screens/ShiftOpenScreen.js +75 -19
  76. package/dist/screens/index.d.ts +8 -0
  77. package/dist/screens/index.js +9 -0
  78. package/dist/shell/index.d.ts +2 -0
  79. package/dist/shell/pos-shell.d.ts +41 -8
  80. package/dist/shell/pos-shell.js +25 -18
  81. package/dist/state/index.d.ts +3 -0
  82. package/dist/state/index.js +4 -0
  83. package/dist/state/pos-context.d.ts +13 -0
  84. package/dist/state/pos-state.d.ts +49 -0
  85. package/dist/state/pos-state.js +6 -7
  86. package/dist/utils/pos-helpers.js +137 -34
  87. package/package.json +112 -92
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { formatBdt } from "../../lib/money.js";
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
- label: formatVariantLabel(variant.attributes),
25
- price: calculateVariantPrice(getPosPrice(product), variant.priceModifier),
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: ", formatBdt(getPosPrice(product))]
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-left",
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-right",
175
+ className: "text-end",
134
176
  children: [/* @__PURE__ */ jsx("p", {
135
177
  className: "font-bold",
136
- children: formatBdt(variant.price)
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 ? "+" : "", formatBdt(variant.priceModifier)]
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 mr-1.5" }), "Add Charge"]
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: "Amount (BDT)"
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 { formatBdt } from "../../../lib/money.js";
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 ml-auto text-xs",
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-right",
89
+ className: "text-end",
90
90
  children: [/* @__PURE__ */ jsx("p", {
91
91
  className: "font-bold",
92
- children: formatBdt(item.lineTotal)
92
+ children: formatMajor(item.lineTotal)
93
93
  }), /* @__PURE__ */ jsxs("p", {
94
94
  className: "text-xs text-muted-foreground",
95
- children: [formatBdt(item.unitPrice), " each"]
95
+ children: [formatMajor(item.unitPrice), " each"]
96
96
  })]
97
97
  })
98
98
  ]
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { formatBdt } from "../../../lib/money.js";
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: formatBdt(subtotal)
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: ["-", formatBdt(discount)]
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: ["-", formatBdt(tierDiscount)]
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: ["-", formatBdt(redemptionDiscount)]
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: formatBdt(total) })]
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 mr-1" }), "Clear"]
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 left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground",
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: "pl-8",
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: "ml-1 hover:text-red-500 transition-colors",
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: "Discount (BDT)",
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 mr-2 text-amber-500" }), "Manager authorization required"]
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 { formatBdt } from "../../../lib/money.js";
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, pointsPerBdt, estimatedDiscount, error, value, onChange, onUseMax }) {
9
+ function PointsRedemptionSection({ enabled, pointsBalance, minRedeemPoints, maxRedeemPoints, pointValueMinor, estimatedDiscount, error, value, onChange, onUseMax }) {
10
10
  if (!enabled) return null;
11
- const helperText = maxRedeemPoints > 0 ? `Up to ${maxRedeemPoints} pts (${formatBdt(maxRedeemPoints / pointsPerBdt)})` : "Not eligible for redemption";
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: ["-", formatBdt(estimatedDiscount)] })]
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 };
@@ -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
- export { type BarcodeScanner, type CashDrawer, HardwareProvider, type PaymentTerminal, type PosHardware, type PrinterAvailability, type PrinterConfig, RECEIPT_DOM_ID, type ReceiptLine, type ReceiptPayload, type ReceiptPayment, type ReceiptPrinter, type ScanEvent, type TerminalChargeInput, type TerminalChargeResult, createTauriHardware, createWebHardware, isTauri, readPrinterConfig, useBarcodeScan, useHardware, usePrinterAvailability, usePrinterConfig };
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 };
@@ -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 { RECEIPT_DOM_ID, createWebHardware } from "./web-adapter.js";
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 };
@@ -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
- /** Print a structured receipt. Adapter handles ESC/POS encoding or browser print. */
61
- print(payload: ReceiptPayload): Promise<void>;
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<void>;
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 };
@@ -1,5 +1,4 @@
1
1
  import { PosHardware } from "./ports.js";
2
-
3
2
  //#region src/hardware/tauri-adapter.d.ts
4
3
  declare function isTauri(): boolean;
5
4
  declare function createTauriHardware(): PosHardware;