@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.
- 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,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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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:
|
|
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(
|
|
587
|
+
children: [/* @__PURE__ */ jsxs(LoadingButton, {
|
|
105
588
|
variant: "outline",
|
|
106
589
|
onClick: handleTest,
|
|
107
|
-
|
|
108
|
-
|
|
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__ */
|
|
150
|
-
|
|
151
|
-
|
|
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__ */
|
|
154
|
-
|
|
155
|
-
|
|
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 };
|