@classytic/pos-ui 0.2.0 → 1.3.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,31 +1,514 @@
1
1
  "use client";
2
2
 
3
+ import { usePosBranch } from "../runtime/branch-port.js";
3
4
  import { usePrinterConfig } from "../hardware/use-printer-config.js";
4
5
  import { isTauri } from "../hardware/tauri-adapter.js";
6
+ import { useAgentPairing } from "../hardware/agent-pairing.js";
5
7
  import { useHardware } from "../hardware/context.js";
6
8
  import { useForm } from "../node_modules/react-hook-form/dist/index.esm.js";
7
9
  import { useCallback, useEffect, useState } from "react";
8
- import { DialogWrapper } from "@classytic/fluid/client/core";
10
+ import { DialogWrapper, EmptyState, LoadingButton, StatusBanner } from "@classytic/fluid/client/core";
9
11
  import { jsx, jsxs } from "react/jsx-runtime";
10
- import { AlertTriangle, Check, Loader2, Printer } from "lucide-react";
12
+ import { AlertTriangle, Check, Link2, Printer, ScanLine, Usb, Wifi } from "lucide-react";
11
13
  import { Button } from "@/components/ui/button";
14
+ import { Input } from "@/components/ui/input";
12
15
  import { FormInput } from "@classytic/fluid/forms";
16
+ import { Label } from "@/components/ui/label";
17
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
18
+ import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@/components/ui/item";
19
+ import { DEVICE_KINDS, useDeviceActions, useIssueDeviceToken } from "@classytic/commerce-sdk/iot";
20
+ import { getBaseUrl } from "@classytic/commerce-sdk/core";
13
21
 
14
22
  //#region src/components/PrinterSettingsDialog.tsx
15
23
  /**
16
24
  * PrinterSettingsDialog — runtime printer config for the cashier.
17
25
  *
18
- * Persists `{ host, port, timeoutMs }` to localStorage via
19
- * `usePrinterConfig`. The Tauri adapter reads the same key on every
20
- * invoke, so saved settings take effect immediately on the next print
21
- * — no app restart, no env-var changes.
26
+ * Two independent modes, chosen by host at runtime — never both:
22
27
  *
23
- * Browser users see this dialog too (via the topbar action menu) but
24
- * it has no effect on browser printing `window.print()` runs against
25
- * whatever printer the OS has configured. The dialog is informational
26
- * for browser, functional for Tauri.
28
+ * - **Tauri**: persists `{ host, port, timeoutMs }` to localStorage via
29
+ * `usePrinterConfig`. The native adapter reads the same key on every
30
+ * invoke, so saved settings take effect immediately on the next print.
31
+ * - **Browser + `@classytic/pos-agent`** (the plan's Stream 1.0/1.4):
32
+ * pairing is BACKEND-MEDIATED, entirely through `@classytic/commerce-sdk`'s
33
+ * existing `iot/` hooks — never a raw fetch to `/devices`:
34
+ * 1. `useDeviceActions().create` registers a `Device` row
35
+ * (`vendor: 'escpos'`, `kind: DEVICE_KINDS.printer`, this branch as
36
+ * `scope`).
37
+ * 2. `useIssueDeviceToken()` mints the one-time bearer secret.
38
+ * 3. The plaintext token is handed to the LOCAL `pos-agent` via its own
39
+ * `POST /v1/pair` (a different, narrower concern — see
40
+ * `@classytic/pos-agent/protocol`'s pairing module).
41
+ * 4. The LOCAL token `pos-agent` returns is stored via
42
+ * `useAgentPairing()` — `createWebHardware()`'s composite adapter
43
+ * picks it up on the very next print, no remount needed.
44
+ * No query fires eagerly: every network call here is a MUTATION, run
45
+ * only on an explicit button press — never a background poll, and
46
+ * nothing fires at all while this dialog is closed or unpaired.
47
+ *
48
+ * Browser users with no agent paired still see this dialog (via the topbar
49
+ * action menu); it is purely informational until they pair one.
27
50
  */
28
51
  function PrinterSettingsDialog({ open, onOpenChange }) {
52
+ return typeof window !== "undefined" && isTauri() ? /* @__PURE__ */ jsx(TauriPrinterConfigDialog, {
53
+ open,
54
+ onOpenChange
55
+ }) : /* @__PURE__ */ jsx(AgentPairingDialog, {
56
+ open,
57
+ onOpenChange
58
+ });
59
+ }
60
+ function AgentPairingDialog({ open, onOpenChange }) {
61
+ const { pairing, setPairing } = useAgentPairing();
62
+ const { printer } = useHardware();
63
+ const branch = usePosBranch();
64
+ const deviceActions = useDeviceActions();
65
+ const issueToken = useIssueDeviceToken();
66
+ const [probe, setProbe] = useState({ kind: "idle" });
67
+ const [pairState, setPairState] = useState({ kind: "idle" });
68
+ const form = useForm({ defaultValues: {
69
+ agentUrl: pairing?.agentUrl ?? "http://127.0.0.1:9191",
70
+ label: pairing?.sn ?? ""
71
+ } });
72
+ useEffect(() => {
73
+ if (!open) return;
74
+ form.reset({
75
+ agentUrl: pairing?.agentUrl ?? "http://127.0.0.1:9191",
76
+ label: pairing?.sn ?? ""
77
+ });
78
+ setProbe({ kind: "idle" });
79
+ setPairState({ kind: "idle" });
80
+ }, [
81
+ open,
82
+ pairing,
83
+ form
84
+ ]);
85
+ const handlePair = useCallback(async (values) => {
86
+ const agentUrl = values.agentUrl.trim().replace(/\/+$/, "");
87
+ const label = values.label.trim();
88
+ if (!agentUrl || !label) {
89
+ setPairState({
90
+ kind: "error",
91
+ message: "Agent URL and a printer label are required"
92
+ });
93
+ return;
94
+ }
95
+ if (!branch?.id) {
96
+ setPairState({
97
+ kind: "error",
98
+ message: "No active branch — cannot register a device"
99
+ });
100
+ return;
101
+ }
102
+ setPairState({ kind: "pairing" });
103
+ try {
104
+ const device = await deviceActions.create({ data: {
105
+ sn: label,
106
+ vendor: "escpos",
107
+ kind: DEVICE_KINDS.printer,
108
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
109
+ scope: branch.id,
110
+ name: label
111
+ } });
112
+ const issued = await issueToken.mutateAsync({ id: device._id });
113
+ const { pairAgent } = await import("@classytic/pos-agent/client");
114
+ const heartbeatUrl = `${getBaseUrl()}/pos/devices/heartbeat`;
115
+ const res = await pairAgent({ baseUrl: agentUrl }, {
116
+ deviceId: device._id,
117
+ sn: issued.sn,
118
+ registryToken: issued.token,
119
+ heartbeatUrl
120
+ });
121
+ if (!res.ok) {
122
+ setPairState({
123
+ kind: "error",
124
+ message: res.timedOut ? "pos-agent did not respond — is it running?" : res.error?.message ?? "Pairing failed"
125
+ });
126
+ return;
127
+ }
128
+ setPairing({
129
+ agentUrl,
130
+ deviceId: device._id,
131
+ sn: issued.sn,
132
+ localToken: res.data.localToken,
133
+ pairedAt: res.data.pairedAt
134
+ });
135
+ setPairState({ kind: "idle" });
136
+ setProbe({ kind: "ok" });
137
+ } catch (err) {
138
+ setPairState({
139
+ kind: "error",
140
+ message: err instanceof Error ? err.message : "Pairing failed"
141
+ });
142
+ }
143
+ }, [
144
+ branch?.id,
145
+ deviceActions,
146
+ issueToken,
147
+ setPairing
148
+ ]);
149
+ const handleTest = useCallback(async () => {
150
+ setProbe({ kind: "probing" });
151
+ try {
152
+ const ok = await printer.isAvailable();
153
+ setProbe(ok ? { kind: "ok" } : {
154
+ kind: "fail",
155
+ reason: "Printer not reachable"
156
+ });
157
+ } catch (err) {
158
+ setProbe({
159
+ kind: "fail",
160
+ reason: err instanceof Error ? err.message : "Probe failed"
161
+ });
162
+ }
163
+ }, [printer]);
164
+ const handleUnpair = useCallback(() => {
165
+ setPairing(null);
166
+ setProbe({ kind: "idle" });
167
+ }, [setPairing]);
168
+ return /* @__PURE__ */ jsxs(DialogWrapper, {
169
+ open,
170
+ onOpenChange,
171
+ title: "Printer settings",
172
+ description: pairing ? `Paired to "${pairing.sn}" via pos-agent at ${pairing.agentUrl}.` : "Pair a local pos-agent to print receipts to a network ESC/POS printer. Without one, receipts print via the browser's print dialog.",
173
+ footer: /* @__PURE__ */ jsxs("div", {
174
+ className: "flex justify-between gap-2 w-full",
175
+ children: [pairing ? /* @__PURE__ */ jsxs("div", {
176
+ className: "flex gap-2",
177
+ children: [/* @__PURE__ */ jsxs(LoadingButton, {
178
+ variant: "outline",
179
+ onClick: handleTest,
180
+ isLoading: probe.kind === "probing",
181
+ loadingText: "Testing…",
182
+ children: [/* @__PURE__ */ jsx(Printer, { className: "h-4 w-4" }), "Test connection"]
183
+ }), /* @__PURE__ */ jsx(Button, {
184
+ variant: "ghost",
185
+ onClick: handleUnpair,
186
+ children: "Unpair"
187
+ })]
188
+ }) : /* @__PURE__ */ jsx("div", {}), /* @__PURE__ */ jsxs("div", {
189
+ className: "flex gap-2",
190
+ children: [/* @__PURE__ */ jsx(Button, {
191
+ variant: "ghost",
192
+ onClick: () => onOpenChange(false),
193
+ children: "Close"
194
+ }), /* @__PURE__ */ jsxs(LoadingButton, {
195
+ onClick: form.handleSubmit(handlePair),
196
+ isLoading: pairState.kind === "pairing",
197
+ loadingText: "Pairing…",
198
+ children: [/* @__PURE__ */ jsx(Link2, { className: "h-4 w-4" }), pairing ? "Re-pair" : "Pair printer"]
199
+ })]
200
+ })]
201
+ }),
202
+ children: [/* @__PURE__ */ jsxs("form", {
203
+ onSubmit: form.handleSubmit(handlePair),
204
+ className: "space-y-4",
205
+ children: [
206
+ /* @__PURE__ */ jsx(FormInput, {
207
+ control: form.control,
208
+ name: "agentUrl",
209
+ label: "Agent URL",
210
+ placeholder: "http://127.0.0.1:9191",
211
+ helperText: "Where the local pos-agent process is listening on THIS till.",
212
+ required: true
213
+ }),
214
+ /* @__PURE__ */ jsx(FormInput, {
215
+ control: form.control,
216
+ name: "label",
217
+ label: "Printer label",
218
+ placeholder: "TILL-1-PRINTER",
219
+ helperText: "A name for this printer — becomes its registry serial number.",
220
+ required: true
221
+ }),
222
+ pairState.kind === "error" && /* @__PURE__ */ jsx(StatusBanner, {
223
+ variant: "warning",
224
+ icon: AlertTriangle,
225
+ children: pairState.message
226
+ }),
227
+ probe.kind === "ok" && /* @__PURE__ */ jsx(StatusBanner, {
228
+ variant: "success",
229
+ icon: Check,
230
+ children: "Printer responded."
231
+ }),
232
+ probe.kind === "fail" && /* @__PURE__ */ jsx(StatusBanner, {
233
+ variant: "warning",
234
+ icon: AlertTriangle,
235
+ children: probe.reason
236
+ })
237
+ ]
238
+ }), pairing && /* @__PURE__ */ jsx("div", {
239
+ className: "mt-4 pt-4 border-t space-y-3",
240
+ children: /* @__PURE__ */ jsx(ConnectionSection, { pairing })
241
+ })]
242
+ });
243
+ }
244
+ /**
245
+ * Sets the printer's actual TRANSPORT (`PATCH /v1/devices/:id/config`) —
246
+ * separate from pairing above, per the plan's Stream 1.2 ("local settings
247
+ * endpoints only after pairing"). Requires `pairing.localToken`, which only
248
+ * exists once the pairing step above has succeeded.
249
+ */
250
+ function ConnectionSection({ pairing }) {
251
+ const [kind, setKind] = useState("tcp");
252
+ const [host, setHost] = useState("");
253
+ const [port, setPort] = useState("");
254
+ const [selectedUsb, setSelectedUsb] = useState(null);
255
+ const [scan, setScan] = useState({ kind: "idle" });
256
+ const [save, setSave] = useState({ kind: "idle" });
257
+ const [paperWidthMm, setPaperWidthMm] = useState(null);
258
+ const [timeoutMs, setTimeoutMs] = useState("");
259
+ const handleScan = useCallback(async () => {
260
+ setScan({ kind: "scanning" });
261
+ try {
262
+ const { listUsbDevices } = await import("@classytic/pos-agent/client");
263
+ const res = await listUsbDevices({
264
+ baseUrl: pairing.agentUrl,
265
+ localToken: pairing.localToken
266
+ });
267
+ if (!res.ok) {
268
+ setScan({
269
+ kind: "error",
270
+ message: res.timedOut ? "pos-agent did not respond" : res.error?.message ?? "USB support is not configured on this agent"
271
+ });
272
+ return;
273
+ }
274
+ setScan({
275
+ kind: "done",
276
+ devices: res.data.devices
277
+ });
278
+ } catch (err) {
279
+ setScan({
280
+ kind: "error",
281
+ message: err instanceof Error ? err.message : "Scan failed"
282
+ });
283
+ }
284
+ }, [pairing.agentUrl, pairing.localToken]);
285
+ const handleSaveConnection = useCallback(async () => {
286
+ setSave({ kind: "saving" });
287
+ try {
288
+ const { setDeviceConfig } = await import("@classytic/pos-agent/client");
289
+ const timeoutMsNum = Number(timeoutMs);
290
+ const shared = {
291
+ ...paperWidthMm !== null ? { paperWidthMm } : {},
292
+ ...Number.isFinite(timeoutMsNum) && timeoutMsNum > 0 ? { timeoutMs: timeoutMsNum } : {}
293
+ };
294
+ const body = kind === "tcp" ? (() => {
295
+ const trimmedHost = host.trim();
296
+ if (!trimmedHost) throw new Error("Enter a printer host");
297
+ const portNum = Number(port);
298
+ return {
299
+ kind: "tcp",
300
+ host: trimmedHost,
301
+ ...Number.isFinite(portNum) && portNum > 0 ? { port: portNum } : {},
302
+ ...shared
303
+ };
304
+ })() : (() => {
305
+ if (!selectedUsb) throw new Error("Select a USB device first");
306
+ return {
307
+ kind: "usb",
308
+ vendorId: selectedUsb.vendorId,
309
+ productId: selectedUsb.productId,
310
+ ...shared
311
+ };
312
+ })();
313
+ const res = await setDeviceConfig({
314
+ baseUrl: pairing.agentUrl,
315
+ localToken: pairing.localToken
316
+ }, pairing.deviceId, body);
317
+ if (!res.ok) {
318
+ setSave({
319
+ kind: "error",
320
+ message: res.timedOut ? "pos-agent did not respond" : res.error?.message ?? "Save failed"
321
+ });
322
+ return;
323
+ }
324
+ setSave({ kind: "saved" });
325
+ } catch (err) {
326
+ setSave({
327
+ kind: "error",
328
+ message: err instanceof Error ? err.message : "Save failed"
329
+ });
330
+ }
331
+ }, [
332
+ kind,
333
+ host,
334
+ port,
335
+ selectedUsb,
336
+ paperWidthMm,
337
+ timeoutMs,
338
+ pairing.agentUrl,
339
+ pairing.localToken,
340
+ pairing.deviceId
341
+ ]);
342
+ return /* @__PURE__ */ jsxs("div", {
343
+ className: "space-y-3",
344
+ children: [
345
+ /* @__PURE__ */ jsx("div", {
346
+ className: "text-sm font-medium",
347
+ children: "Connection"
348
+ }),
349
+ /* @__PURE__ */ jsxs(ToggleGroup, {
350
+ value: [kind],
351
+ onValueChange: (next) => {
352
+ const value = next[0];
353
+ if (value === "tcp" || value === "usb") setKind(value);
354
+ },
355
+ variant: "outline",
356
+ size: "sm",
357
+ children: [/* @__PURE__ */ jsxs(ToggleGroupItem, {
358
+ value: "tcp",
359
+ "aria-label": "Network printer",
360
+ children: [/* @__PURE__ */ jsx(Wifi, { className: "h-4 w-4" }), "Network"]
361
+ }), /* @__PURE__ */ jsxs(ToggleGroupItem, {
362
+ value: "usb",
363
+ "aria-label": "USB printer",
364
+ children: [/* @__PURE__ */ jsx(Usb, { className: "h-4 w-4" }), "USB"]
365
+ })]
366
+ }),
367
+ kind === "tcp" ? /* @__PURE__ */ jsxs("div", {
368
+ className: "grid grid-cols-2 gap-3",
369
+ children: [/* @__PURE__ */ jsxs("div", {
370
+ className: "space-y-1",
371
+ children: [/* @__PURE__ */ jsx(Label, {
372
+ htmlFor: "printer-host",
373
+ className: "text-xs text-muted-foreground",
374
+ children: "Printer host"
375
+ }), /* @__PURE__ */ jsx(Input, {
376
+ id: "printer-host",
377
+ placeholder: "192.168.1.100",
378
+ value: host,
379
+ onChange: (e) => setHost(e.target.value)
380
+ })]
381
+ }), /* @__PURE__ */ jsxs("div", {
382
+ className: "space-y-1",
383
+ children: [/* @__PURE__ */ jsx(Label, {
384
+ htmlFor: "printer-port",
385
+ className: "text-xs text-muted-foreground",
386
+ children: "Port (default 9100)"
387
+ }), /* @__PURE__ */ jsx(Input, {
388
+ id: "printer-port",
389
+ placeholder: "9100",
390
+ value: port,
391
+ onChange: (e) => setPort(e.target.value)
392
+ })]
393
+ })]
394
+ }) : /* @__PURE__ */ jsxs("div", {
395
+ className: "space-y-2",
396
+ children: [
397
+ /* @__PURE__ */ jsxs(LoadingButton, {
398
+ type: "button",
399
+ variant: "outline",
400
+ size: "sm",
401
+ onClick: handleScan,
402
+ isLoading: scan.kind === "scanning",
403
+ loadingText: "Scanning…",
404
+ children: [/* @__PURE__ */ jsx(ScanLine, { className: "h-4 w-4" }), "Scan for USB printers"]
405
+ }),
406
+ scan.kind === "done" && (scan.devices.filter((d) => d.isPrinterClass).length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
407
+ variant: "compact",
408
+ icon: /* @__PURE__ */ jsx(Usb, { className: "h-5 w-5" }),
409
+ title: "No USB Printer-class device found",
410
+ description: "It may need a WinUSB driver bound (e.g. via Zadig) before pos-agent can access it — its OS driver commonly holds the interface exclusively."
411
+ }) : /* @__PURE__ */ jsx("div", {
412
+ role: "list",
413
+ className: "space-y-1.5",
414
+ children: scan.devices.filter((d) => d.isPrinterClass).map((d) => {
415
+ const isSelected = selectedUsb?.vendorId === d.vendorId && selectedUsb?.productId === d.productId;
416
+ return /* @__PURE__ */ jsxs(Item, {
417
+ render: /* @__PURE__ */ jsx("button", {
418
+ type: "button",
419
+ onClick: () => setSelectedUsb(d)
420
+ }),
421
+ variant: isSelected ? "outline" : "default",
422
+ size: "sm",
423
+ className: isSelected ? "border-primary bg-primary/5" : void 0,
424
+ children: [
425
+ /* @__PURE__ */ jsx(ItemMedia, {
426
+ variant: "icon",
427
+ children: /* @__PURE__ */ jsx(Usb, { className: "h-4 w-4" })
428
+ }),
429
+ /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: (d.manufacturer ?? "Unknown") + " " + (d.product ?? "device") }), /* @__PURE__ */ jsxs(ItemDescription, { children: [
430
+ d.vendorId.toString(16),
431
+ ":",
432
+ d.productId.toString(16)
433
+ ] })] }),
434
+ isSelected && /* @__PURE__ */ jsx(ItemActions, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-primary" }) })
435
+ ]
436
+ }, `${d.vendorId}:${d.productId}`);
437
+ })
438
+ })),
439
+ scan.kind === "error" && /* @__PURE__ */ jsx(StatusBanner, {
440
+ variant: "warning",
441
+ icon: AlertTriangle,
442
+ children: scan.message
443
+ })
444
+ ]
445
+ }),
446
+ /* @__PURE__ */ jsxs("div", {
447
+ className: "grid grid-cols-2 gap-3",
448
+ children: [/* @__PURE__ */ jsxs("div", {
449
+ className: "space-y-1",
450
+ children: [/* @__PURE__ */ jsx(Label, {
451
+ className: "text-xs text-muted-foreground",
452
+ children: "Paper width"
453
+ }), /* @__PURE__ */ jsxs(ToggleGroup, {
454
+ value: paperWidthMm !== null ? [String(paperWidthMm)] : [],
455
+ onValueChange: (next) => {
456
+ const value = next[0];
457
+ setPaperWidthMm(value === "58" || value === "80" ? Number(value) : null);
458
+ },
459
+ variant: "outline",
460
+ size: "sm",
461
+ children: [/* @__PURE__ */ jsx(ToggleGroupItem, {
462
+ value: "58",
463
+ children: "58mm"
464
+ }), /* @__PURE__ */ jsx(ToggleGroupItem, {
465
+ value: "80",
466
+ children: "80mm"
467
+ })]
468
+ })]
469
+ }), /* @__PURE__ */ jsxs("div", {
470
+ className: "space-y-1",
471
+ children: [/* @__PURE__ */ jsx(Label, {
472
+ htmlFor: "printer-timeout",
473
+ className: "text-xs text-muted-foreground",
474
+ children: "Timeout (ms)"
475
+ }), /* @__PURE__ */ jsx(Input, {
476
+ id: "printer-timeout",
477
+ placeholder: "2000",
478
+ value: timeoutMs,
479
+ onChange: (e) => setTimeoutMs(e.target.value)
480
+ })]
481
+ })]
482
+ }),
483
+ /* @__PURE__ */ jsxs("div", {
484
+ className: "flex items-center gap-2",
485
+ children: [
486
+ /* @__PURE__ */ jsx(LoadingButton, {
487
+ type: "button",
488
+ size: "sm",
489
+ onClick: handleSaveConnection,
490
+ isLoading: save.kind === "saving",
491
+ loadingText: "Saving…",
492
+ children: "Save connection"
493
+ }),
494
+ save.kind === "saved" && /* @__PURE__ */ jsxs("span", {
495
+ className: "flex items-center gap-1 text-sm text-green-700 dark:text-green-300",
496
+ children: [/* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }), " Saved"]
497
+ }),
498
+ save.kind === "error" && /* @__PURE__ */ jsxs("span", {
499
+ className: "flex items-center gap-1 text-sm text-amber-700 dark:text-amber-300",
500
+ children: [
501
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }),
502
+ " ",
503
+ save.message
504
+ ]
505
+ })
506
+ ]
507
+ })
508
+ ]
509
+ });
510
+ }
511
+ function TauriPrinterConfigDialog({ open, onOpenChange }) {
29
512
  const { config, setConfig } = usePrinterConfig();
30
513
  const { printer } = useHardware();
31
514
  const [probe, setProbe] = useState({ kind: "idle" });
@@ -98,14 +581,15 @@ function PrinterSettingsDialog({ open, onOpenChange }) {
98
581
  open,
99
582
  onOpenChange,
100
583
  title: "Printer settings",
101
- description: typeof window !== "undefined" && isTauri() ? "Configure the network thermal printer for receipts and cash-drawer kicks." : "Browser POS uses the OS print dialog. These settings only apply in the desktop (Tauri) shell.",
584
+ description: "Configure the network thermal printer for receipts and cash-drawer kicks.",
102
585
  footer: /* @__PURE__ */ jsxs("div", {
103
586
  className: "flex justify-between gap-2 w-full",
104
- children: [/* @__PURE__ */ jsxs(Button, {
587
+ children: [/* @__PURE__ */ jsxs(LoadingButton, {
105
588
  variant: "outline",
106
589
  onClick: handleTest,
107
- disabled: probe.kind === "probing",
108
- children: [probe.kind === "probing" ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }) : /* @__PURE__ */ jsx(Printer, { className: "h-4 w-4 mr-2" }), "Test connection"]
590
+ isLoading: probe.kind === "probing",
591
+ loadingText: "Testing…",
592
+ children: [/* @__PURE__ */ jsx(Printer, { className: "h-4 w-4" }), "Test connection"]
109
593
  }), /* @__PURE__ */ jsxs("div", {
110
594
  className: "flex gap-2",
111
595
  children: [/* @__PURE__ */ jsx(Button, {
@@ -146,13 +630,15 @@ function PrinterSettingsDialog({ open, onOpenChange }) {
146
630
  helperText: "Default 2000"
147
631
  })]
148
632
  }),
149
- probe.kind === "ok" && /* @__PURE__ */ jsxs("div", {
150
- className: "flex items-center gap-2 text-sm text-green-700 dark:text-green-300",
151
- children: [/* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }), "Printer responded — saved."]
633
+ probe.kind === "ok" && /* @__PURE__ */ jsx(StatusBanner, {
634
+ variant: "success",
635
+ icon: Check,
636
+ children: "Printer responded — saved."
152
637
  }),
153
- probe.kind === "fail" && /* @__PURE__ */ jsxs("div", {
154
- className: "flex items-start gap-2 text-sm text-amber-700 dark:text-amber-300",
155
- children: [/* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), /* @__PURE__ */ jsx("span", { children: probe.reason })]
638
+ probe.kind === "fail" && /* @__PURE__ */ jsx(StatusBanner, {
639
+ variant: "warning",
640
+ icon: AlertTriangle,
641
+ children: probe.reason
156
642
  })
157
643
  ]
158
644
  })
@@ -0,0 +1,11 @@
1
+ //#region src/components/ReceiptDeliveryDialog.d.ts
2
+ interface ReceiptDeliveryDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ orderId: string | null;
6
+ customerEmail?: string;
7
+ customerPhone?: string;
8
+ }
9
+ declare function ReceiptDeliveryDialog({ open, onOpenChange, orderId, customerEmail, customerPhone }: ReceiptDeliveryDialogProps): import("react").JSX.Element;
10
+ //#endregion
11
+ export { ReceiptDeliveryDialog };
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { DialogWrapper } from "@classytic/fluid/client/core";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Loader2, Mail, Send, Smartphone } from "lucide-react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { getReceiptLocale } from "@classytic/commerce-sdk/client";
9
+ import { toast } from "sonner";
10
+ import { presentError } from "@classytic/fluid/error-contract";
11
+ import { Input } from "@/components/ui/input";
12
+
13
+ //#region src/components/ReceiptDeliveryDialog.tsx
14
+ /**
15
+ * ReceiptDeliveryDialog — Send receipt via email or SMS.
16
+ *
17
+ * Uses fluid's DialogWrapper. Opt-in only — backend stub ready for
18
+ * POST /pos/orders/:id/send-receipt when notifications are wired.
19
+ */
20
+ function ReceiptDeliveryDialog({ open, onOpenChange, orderId, customerEmail, customerPhone }) {
21
+ const [method, setMethod] = useState("email");
22
+ const [destination, setDestination] = useState(customerEmail || customerPhone || "");
23
+ const [isSending, setIsSending] = useState(false);
24
+ const handleSend = useCallback(async () => {
25
+ if (!destination.trim() || !orderId) return;
26
+ setIsSending(true);
27
+ try {
28
+ toast.info(`Receipt delivery (${method}) is not yet connected. Destination: ${destination}`);
29
+ onOpenChange(false);
30
+ } catch (err) {
31
+ const { title, message } = presentError(err, "The receipt was not sent. Try again.", { fallbackTitle: "Could not send the receipt" });
32
+ toast.error(title, { description: message });
33
+ } finally {
34
+ setIsSending(false);
35
+ }
36
+ }, [
37
+ orderId,
38
+ method,
39
+ destination,
40
+ onOpenChange
41
+ ]);
42
+ const footer = /* @__PURE__ */ jsxs("div", {
43
+ className: "flex justify-end gap-2",
44
+ children: [/* @__PURE__ */ jsx(Button, {
45
+ variant: "outline",
46
+ size: "sm",
47
+ onClick: () => onOpenChange(false),
48
+ children: "Cancel"
49
+ }), /* @__PURE__ */ jsxs(Button, {
50
+ size: "sm",
51
+ onClick: handleSend,
52
+ disabled: !destination.trim() || isSending,
53
+ children: [isSending ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 me-1.5 animate-spin" }) : /* @__PURE__ */ jsx(Send, { className: "h-4 w-4 me-1.5" }), "Send"]
54
+ })]
55
+ });
56
+ return /* @__PURE__ */ jsx(DialogWrapper, {
57
+ open,
58
+ onOpenChange,
59
+ title: "Send Receipt",
60
+ description: "Send a digital receipt to the customer.",
61
+ size: "sm",
62
+ footer,
63
+ children: /* @__PURE__ */ jsxs("div", {
64
+ className: "space-y-4",
65
+ children: [/* @__PURE__ */ jsxs("div", {
66
+ className: "flex gap-2",
67
+ children: [/* @__PURE__ */ jsxs(Button, {
68
+ variant: method === "email" ? "default" : "outline",
69
+ size: "sm",
70
+ className: "flex-1",
71
+ onClick: () => {
72
+ setMethod("email");
73
+ setDestination(customerEmail || "");
74
+ },
75
+ children: [/* @__PURE__ */ jsx(Mail, { className: "h-4 w-4 me-1.5" }), "Email"]
76
+ }), /* @__PURE__ */ jsxs(Button, {
77
+ variant: method === "sms" ? "default" : "outline",
78
+ size: "sm",
79
+ className: "flex-1",
80
+ onClick: () => {
81
+ setMethod("sms");
82
+ setDestination(customerPhone || "");
83
+ },
84
+ children: [/* @__PURE__ */ jsx(Smartphone, { className: "h-4 w-4 me-1.5" }), "SMS"]
85
+ })]
86
+ }), /* @__PURE__ */ jsx(Input, {
87
+ type: method === "email" ? "email" : "tel",
88
+ placeholder: method === "email" ? "customer@example.com" : getReceiptLocale().phonePlaceholder,
89
+ value: destination,
90
+ onChange: (e) => setDestination(e.target.value),
91
+ autoFocus: true
92
+ })]
93
+ })
94
+ });
95
+ }
96
+
97
+ //#endregion
98
+ export { ReceiptDeliveryDialog };
@@ -0,0 +1,10 @@
1
+ //#region src/components/ReceiptReprintDialog.d.ts
2
+ interface ReceiptReprintDialogProps {
3
+ /** Raw order document from `usePosOrderDetail` — null while loading or when closed. */
4
+ order: unknown;
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ }
8
+ declare function ReceiptReprintDialog({ order, open, onOpenChange }: ReceiptReprintDialogProps): import("react").JSX.Element;
9
+ //#endregion
10
+ export { ReceiptReprintDialog };