@flowselections/floriday-klanten-module 1.0.35 → 1.0.36

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 (73) hide show
  1. package/dist-lib/components/klanten/BewerkKlant.d.ts.map +1 -1
  2. package/dist-lib/components/klanten/BewerkKlant.js +21 -1
  3. package/dist-lib/components/klanten/BulkStickerUploadDialog.d.ts +14 -0
  4. package/dist-lib/components/klanten/BulkStickerUploadDialog.d.ts.map +1 -0
  5. package/dist-lib/components/klanten/BulkStickerUploadDialog.js +231 -0
  6. package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts.map +1 -1
  7. package/dist-lib/components/klanten/CustomerAssortmentTab.js +37 -5
  8. package/dist-lib/components/klanten/CustomerStickerCell.d.ts +7 -0
  9. package/dist-lib/components/klanten/CustomerStickerCell.d.ts.map +1 -0
  10. package/dist-lib/components/klanten/CustomerStickerCell.js +90 -0
  11. package/dist-lib/components/klanten/CustomerStickerFormatDialog.d.ts +7 -0
  12. package/dist-lib/components/klanten/CustomerStickerFormatDialog.d.ts.map +1 -0
  13. package/dist-lib/components/klanten/CustomerStickerFormatDialog.js +36 -0
  14. package/dist-lib/index.d.ts +4 -0
  15. package/dist-lib/index.d.ts.map +1 -1
  16. package/dist-lib/index.js +4 -0
  17. package/dist-lib/integrations/supabase/auth-middleware.d.ts +565 -0
  18. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  19. package/dist-lib/integrations/supabase/client.d.ts +565 -0
  20. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/client.server.d.ts +565 -0
  22. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  23. package/dist-lib/integrations/supabase/types.d.ts +568 -0
  24. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  25. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1695 -0
  26. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
  27. package/dist-lib/lib/accounting.functions.d.ts +1695 -0
  28. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/ai-module.server.d.ts +20 -0
  30. package/dist-lib/lib/ai-module.server.d.ts.map +1 -0
  31. package/dist-lib/lib/ai-module.server.js +86 -0
  32. package/dist-lib/lib/customer-analytics.functions.d.ts +565 -0
  33. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  34. package/dist-lib/lib/customer-assortment.functions.d.ts +565 -0
  35. package/dist-lib/lib/customer-assortment.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/customer-notes.functions.d.ts +1130 -0
  37. package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
  38. package/dist-lib/lib/customer-revenue.functions.d.ts +565 -0
  39. package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/customer-stickers.functions.d.ts +244731 -0
  41. package/dist-lib/lib/customer-stickers.functions.d.ts.map +1 -0
  42. package/dist-lib/lib/customer-stickers.functions.js +467 -0
  43. package/dist-lib/lib/customers.functions.d.ts +5085 -0
  44. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/delivery-locations.functions.d.ts +2825 -0
  46. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/external-customers.functions.d.ts +2260 -0
  48. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  49. package/dist-lib/lib/floriday-assortment.functions.d.ts +565 -0
  50. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
  51. package/dist-lib/lib/floriday-discounts.functions.d.ts +1130 -0
  52. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  53. package/dist-lib/lib/floriday-price-import.functions.d.ts +1130 -0
  54. package/dist-lib/lib/floriday-price-import.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/floriday-trade-items.functions.d.ts +565 -0
  56. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
  57. package/dist-lib/lib/local-discounts.functions.d.ts +3956 -1
  58. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  59. package/dist-lib/lib/logistics-events.functions.d.ts +565 -0
  60. package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
  61. package/dist-lib/lib/logistics-strategy.functions.d.ts +2260 -0
  62. package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
  63. package/dist-lib/lib/overdue-invoices.functions.d.ts +565 -0
  64. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  65. package/dist-lib/lib/price-groups-local.functions.d.ts +7348 -3
  66. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
  67. package/dist-lib/lib/price-groups.functions.d.ts +565 -0
  68. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
  69. package/dist-lib/lib/twinfield-adopt.functions.d.ts +1130 -0
  70. package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
  71. package/dist-lib/lib/twinfield-ledger.functions.d.ts +565 -0
  72. package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
  73. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,MAAM,EACN,QAAQ,EACR,YAAY,GACb,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,+BAsDA"}
1
+ {"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AAgEA,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,MAAM,EACN,QAAQ,EACR,YAAY,GACb,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,+BAuDA"}
@@ -8,6 +8,26 @@ import { getCustomer, updateCustomer } from "../../lib/customers.functions";
8
8
  import { decodeDeep } from "../../lib/decode-html";
9
9
  import { BackLink } from "../../components/klanten/BackLink";
10
10
  import { KlantFormulier, emptyKlantFormValues, } from "../../components/klanten/KlantFormulier";
11
+ import { resolveCustomerKeyForLocalCustomer } from "../../lib/price-groups-local.functions";
12
+ import { getCustomerStickerSettings } from "../../lib/customer-stickers.functions";
13
+ import { CustomerStickerFormatDialog } from "../../components/klanten/CustomerStickerFormatDialog";
14
+ function StickerFormatSection({ customerId }) {
15
+ const resolveKey = useServerFn(resolveCustomerKeyForLocalCustomer);
16
+ const getFormat = useServerFn(getCustomerStickerSettings);
17
+ const { data: keyData } = useQuery({
18
+ queryKey: ["customer-group-key", customerId],
19
+ queryFn: () => resolveKey({ data: { customerId } }),
20
+ });
21
+ const customerKey = keyData?.customerKey ?? null;
22
+ const { data: format } = useQuery({
23
+ queryKey: ["customer-sticker-format", customerKey],
24
+ queryFn: () => getFormat({ data: { customerKey: customerKey } }),
25
+ enabled: !!customerKey,
26
+ });
27
+ if (!customerKey)
28
+ return null;
29
+ return (_jsxs("div", { className: "rounded-lg border p-4 space-y-2", children: [_jsx("h3", { className: "text-sm font-medium", children: "Stickerformaat" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Formaat waarop de stickers van deze klant worden geprint:", " ", _jsx("strong", { className: "text-foreground", children: format ? `${format.width_mm} × ${format.height_mm} mm` : "nog niet ingesteld" })] }), _jsx(CustomerStickerFormatDialog, { customerKey: customerKey, format: format ?? null })] }));
30
+ }
11
31
  /** Zet een klantrij uit de database om naar formulierwaarden (null → ""). */
12
32
  function toFormValues(c) {
13
33
  const out = { ...emptyKlantFormValues };
@@ -59,5 +79,5 @@ export function BewerkKlant({ id, onDone, onCancel, hideBackLink, }) {
59
79
  return (_jsxs("main", { className: "p-6 space-y-3", children: [!hideBackLink && _jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Klant niet gevonden." })] }));
60
80
  }
61
81
  const c = data.customer;
62
- return (_jsx(KlantFormulier, { title: `${c.company_name || "Klant"} bewerken`, description: "Pas de gegevens aan. Wijzigingen worden direct naar Twinfield doorgestuurd.", submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: toFormValues(c), onSubmit: (values) => mut.mutate(values), onCancel: () => onCancel ? onCancel() : navigate({ to: "/klanten/$id", params: { id } }) }));
82
+ return (_jsx(KlantFormulier, { title: `${c.company_name || "Klant"} bewerken`, description: "Pas de gegevens aan. Wijzigingen worden direct naar Twinfield doorgestuurd.", submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: toFormValues(c), onSubmit: (values) => mut.mutate(values), extraSections: _jsx(StickerFormatSection, { customerId: id }), onCancel: () => onCancel ? onCancel() : navigate({ to: "/klanten/$id", params: { id } }) }));
63
83
  }
@@ -0,0 +1,14 @@
1
+ export type BulkProduct = {
2
+ key: string;
3
+ name: string;
4
+ code: string | null;
5
+ vbn: string | null;
6
+ botanical: string | null;
7
+ hasSticker: boolean;
8
+ };
9
+ export declare function BulkStickerUploadDialog({ customerKey, products, onDone, }: {
10
+ customerKey: string;
11
+ products: BulkProduct[];
12
+ onDone: () => void;
13
+ }): import("react").JSX.Element;
14
+ //# sourceMappingURL=BulkStickerUploadDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BulkStickerUploadDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BulkStickerUploadDialog.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAiDF,wBAAgB,uBAAuB,CAAC,EACtC,WAAW,EACX,QAAQ,EACR,MAAM,GACP,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,+BAoYA"}
@@ -0,0 +1,231 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState } from "react";
3
+ import { useServerFn } from "@tanstack/react-start";
4
+ import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@flowselections/core";
5
+ import { FileText, FolderOpen, Loader2, Trash2, Upload, Sparkles } from "lucide-react";
6
+ import { toast } from "sonner";
7
+ import { supabase } from "../../integrations/supabase/client";
8
+ import { ALLOWED_STICKER_MIME, commitStickerBatch, discardStickerBatch, suggestStickerMatches, } from "../../lib/customer-stickers.functions";
9
+ import { friendlyError } from "../../lib/errors";
10
+ import { SearchableSelect } from "./SearchableSelect";
11
+ const BUCKET = "customer-stickers";
12
+ const MAX_SIZE = 10 * 1024 * 1024;
13
+ const ACCEPT = ".pdf,.png,.jpg,.jpeg,.svg";
14
+ function safeKey(value) {
15
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
16
+ }
17
+ function extOf(name) {
18
+ return (/\.([a-z0-9]+)$/i.exec(name)?.[1] ?? "bin").toLowerCase();
19
+ }
20
+ async function makeThumbnail(file) {
21
+ if (!file.type.startsWith("image/"))
22
+ return null;
23
+ try {
24
+ const url = URL.createObjectURL(file);
25
+ const img = await new Promise((resolve, reject) => {
26
+ const el = new Image();
27
+ el.onload = () => resolve(el);
28
+ el.onerror = reject;
29
+ el.src = url;
30
+ });
31
+ const max = 512;
32
+ const scale = Math.min(1, max / Math.max(img.width || max, img.height || max));
33
+ const canvas = document.createElement("canvas");
34
+ canvas.width = Math.max(1, Math.round((img.width || max) * scale));
35
+ canvas.height = Math.max(1, Math.round((img.height || max) * scale));
36
+ const ctx = canvas.getContext("2d");
37
+ if (!ctx)
38
+ return null;
39
+ ctx.fillStyle = "#ffffff";
40
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
41
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
42
+ URL.revokeObjectURL(url);
43
+ return canvas.toDataURL("image/jpeg", 0.7);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ export function BulkStickerUploadDialog({ customerKey, products, onDone, }) {
50
+ const [open, setOpen] = useState(false);
51
+ const [batchId, setBatchId] = useState(null);
52
+ const [rows, setRows] = useState([]);
53
+ const [phase, setPhase] = useState("idle");
54
+ const [progress, setProgress] = useState({ done: 0, total: 0 });
55
+ const folderRef = useRef(null);
56
+ const filesRef = useRef(null);
57
+ const suggest = useServerFn(suggestStickerMatches);
58
+ const commit = useServerFn(commitStickerBatch);
59
+ const discard = useServerFn(discardStickerBatch);
60
+ const productOptions = useMemo(() => products.map((p) => ({
61
+ value: p.key,
62
+ label: p.name,
63
+ search: [p.name, p.code, p.vbn, p.botanical].filter(Boolean).join(" "),
64
+ })), [products]);
65
+ const productByKey = useMemo(() => new Map(products.map((p) => [p.key, p])), [products]);
66
+ const duplicates = useMemo(() => {
67
+ const count = new Map();
68
+ for (const r of rows)
69
+ if (r.tradeItemKey)
70
+ count.set(r.tradeItemKey, (count.get(r.tradeItemKey) ?? 0) + 1);
71
+ return new Set([...count.entries()].filter(([, n]) => n > 1).map(([k]) => k));
72
+ }, [rows]);
73
+ const reset = () => {
74
+ setRows([]);
75
+ setBatchId(null);
76
+ setPhase("idle");
77
+ setProgress({ done: 0, total: 0 });
78
+ };
79
+ const handleFiles = async (fileList) => {
80
+ const all = Array.from(fileList ?? []).filter((f) => ALLOWED_STICKER_MIME.includes((f.type || "")));
81
+ if (all.length === 0) {
82
+ toast.error("Geen bruikbare bestanden gevonden. Gebruik PDF, PNG, JPG of SVG.");
83
+ return;
84
+ }
85
+ const tooBig = all.filter((f) => f.size > MAX_SIZE);
86
+ const files = all.filter((f) => f.size <= MAX_SIZE);
87
+ if (tooBig.length)
88
+ toast.error(`${tooBig.length} bestand(en) zijn groter dan 10 MB en worden overgeslagen.`);
89
+ if (files.length === 0)
90
+ return;
91
+ const id = crypto.randomUUID();
92
+ setBatchId(id);
93
+ setPhase("uploading");
94
+ setProgress({ done: 0, total: files.length });
95
+ const staged = [];
96
+ for (const file of files) {
97
+ const fileId = crypto.randomUUID();
98
+ const path = `${safeKey(customerKey)}/_inbox/${safeKey(id)}/${fileId}.${extOf(file.name)}`;
99
+ const { error } = await supabase.storage
100
+ .from(BUCKET)
101
+ .upload(path, file, { contentType: file.type, upsert: false });
102
+ if (error) {
103
+ toast.error(`${file.name}: ${error.message}`);
104
+ }
105
+ else {
106
+ staged.push({
107
+ fileId,
108
+ fileName: file.name,
109
+ mimeType: file.type,
110
+ fileSize: file.size,
111
+ inboxPath: path,
112
+ thumbnail: await makeThumbnail(file),
113
+ tradeItemKey: null,
114
+ confidence: 0,
115
+ reason: null,
116
+ replaceExisting: false,
117
+ });
118
+ }
119
+ setProgress((p) => ({ ...p, done: p.done + 1 }));
120
+ }
121
+ setRows(staged);
122
+ if (staged.length === 0) {
123
+ reset();
124
+ return;
125
+ }
126
+ setPhase("matching");
127
+ try {
128
+ const res = await suggest({
129
+ data: {
130
+ files: staged.map((s) => ({
131
+ fileId: s.fileId,
132
+ fileName: s.fileName,
133
+ thumbnail: s.thumbnail,
134
+ })),
135
+ products: products.map((p) => ({
136
+ key: p.key,
137
+ name: p.name,
138
+ code: p.code,
139
+ vbn: p.vbn,
140
+ botanical: p.botanical,
141
+ })),
142
+ },
143
+ });
144
+ const byId = new Map(res.suggestions.map((s) => [s.fileId, s]));
145
+ setRows((prev) => prev.map((r) => {
146
+ const s = byId.get(r.fileId);
147
+ return s
148
+ ? { ...r, tradeItemKey: s.tradeItemKey, confidence: s.confidence, reason: s.reason }
149
+ : r;
150
+ }));
151
+ }
152
+ catch (e) {
153
+ toast.error(friendlyError(e, "AI-herkenning mislukt — koppel de stickers handmatig."));
154
+ }
155
+ setPhase("review");
156
+ };
157
+ const close = async (cancel) => {
158
+ if (cancel && batchId && rows.length) {
159
+ try {
160
+ await discard({ data: { customerKey, batchId } });
161
+ }
162
+ catch {
163
+ /* opruimen mag stilletjes falen */
164
+ }
165
+ }
166
+ setOpen(false);
167
+ reset();
168
+ };
169
+ const save = async () => {
170
+ const items = rows
171
+ .filter((r) => r.tradeItemKey)
172
+ .map((r) => ({
173
+ inboxPath: r.inboxPath,
174
+ fileName: r.fileName,
175
+ mimeType: r.mimeType,
176
+ fileSize: r.fileSize,
177
+ tradeItemKey: r.tradeItemKey,
178
+ replaceExisting: r.replaceExisting,
179
+ }));
180
+ if (items.length === 0) {
181
+ toast.error("Koppel eerst minimaal één sticker aan een product.");
182
+ return;
183
+ }
184
+ setPhase("saving");
185
+ try {
186
+ const res = await commit({ data: { customerKey, batchId: batchId, items } });
187
+ const parts = [
188
+ `${res.linked} nieuw`,
189
+ res.replaced ? `${res.replaced} vervangen` : null,
190
+ res.skipped ? `${res.skipped} overgeslagen` : null,
191
+ res.failed.length ? `${res.failed.length} mislukt` : null,
192
+ ].filter(Boolean);
193
+ toast.success(`Stickers opgeslagen: ${parts.join(", ")}`);
194
+ if (res.failed.length) {
195
+ toast.error(res.failed.map((f) => `${f.fileName}: ${f.reason}`).join(" · "));
196
+ }
197
+ onDone();
198
+ setOpen(false);
199
+ reset();
200
+ }
201
+ catch (e) {
202
+ toast.error(friendlyError(e, "Opslaan mislukt"));
203
+ setPhase("review");
204
+ }
205
+ };
206
+ const busy = phase === "uploading" || phase === "matching" || phase === "saving";
207
+ return (_jsxs(_Fragment, { children: [_jsx("input", { ref: folderRef, type: "file", className: "hidden", multiple: true,
208
+ // @ts-expect-error niet-standaard maar breed ondersteund
209
+ webkitdirectory: "", directory: "", onChange: (e) => {
210
+ void handleFiles(e.target.files);
211
+ e.target.value = "";
212
+ } }), _jsx("input", { ref: filesRef, type: "file", className: "hidden", multiple: true, accept: ACCEPT, onChange: (e) => {
213
+ void handleFiles(e.target.files);
214
+ e.target.value = "";
215
+ } }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setOpen(true), children: [_jsx(Upload, { className: "h-4 w-4 mr-1.5" }), "Stickers in bulk"] }), _jsx(Dialog, { open: open, onOpenChange: (v) => (v ? setOpen(true) : void close(true)), children: _jsxs(DialogContent, { className: "max-w-5xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Stickers in bulk uploaden" }) }), phase === "idle" && (_jsxs("div", { className: "rounded-lg border border-dashed p-10 text-center", onDragOver: (e) => e.preventDefault(), onDrop: (e) => {
216
+ e.preventDefault();
217
+ void handleFiles(e.dataTransfer.files);
218
+ }, children: [_jsx("p", { className: "text-sm text-muted-foreground mb-4", children: "Sleep een map of losse stickerbestanden hierheen (PDF, PNG, JPG of SVG, max. 10 MB per bestand)." }), _jsxs("div", { className: "flex justify-center gap-2", children: [_jsxs(Button, { type: "button", onClick: () => folderRef.current?.click(), children: [_jsx(FolderOpen, { className: "h-4 w-4 mr-1.5" }), "Map kiezen"] }), _jsx(Button, { type: "button", variant: "outline", onClick: () => filesRef.current?.click(), children: "Bestanden kiezen" })] })] })), (phase === "uploading" || phase === "matching") && (_jsxs("div", { className: "flex items-center gap-3 p-10 justify-center text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), phase === "uploading"
219
+ ? `Bestanden uploaden… ${progress.done}/${progress.total}`
220
+ : "AI zoekt bij elke sticker het juiste product…"] })), (phase === "review" || phase === "saving") && (_jsx("div", { className: "max-h-[60vh] overflow-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "sticky top-0 bg-background", children: _jsxs("tr", { className: "border-b text-left", children: [_jsx("th", { className: "py-2 w-16", children: "Sticker" }), _jsx("th", { className: "py-2", children: "Bestand" }), _jsx("th", { className: "py-2 w-[260px]", children: "Product" }), _jsx("th", { className: "py-2 w-32", children: "Zekerheid" }), _jsx("th", { className: "py-2 w-40", children: "Bestaand" }), _jsx("th", { className: "py-2 w-10" })] }) }), _jsx("tbody", { children: rows.map((r) => {
221
+ const product = r.tradeItemKey ? productByKey.get(r.tradeItemKey) : null;
222
+ const dup = r.tradeItemKey ? duplicates.has(r.tradeItemKey) : false;
223
+ return (_jsxs("tr", { className: "border-b align-middle", children: [_jsx("td", { className: "py-2", children: r.thumbnail ? (_jsx("img", { src: r.thumbnail, alt: "", className: "h-10 w-10 rounded border object-contain bg-muted" })) : (_jsx("div", { className: "h-10 w-10 rounded border bg-muted flex items-center justify-center", children: _jsx(FileText, { className: "h-4 w-4 text-muted-foreground" }) })) }), _jsxs("td", { className: "py-2 pr-3", children: [_jsx("div", { className: "truncate max-w-[220px]", title: r.fileName, children: r.fileName }), r.reason && (_jsx("div", { className: "text-xs text-muted-foreground truncate max-w-[220px]", children: r.reason }))] }), _jsxs("td", { className: "py-2 pr-3", children: [_jsx(SearchableSelect, { value: r.tradeItemKey ?? "", options: productOptions, placeholder: "Kies product\u2026", onValueChange: (v) => setRows((prev) => prev.map((x) => x.fileId === r.fileId ? { ...x, tradeItemKey: v || null } : x)) }), dup && (_jsx("div", { className: "text-xs text-destructive mt-1", children: "Meerdere stickers voor dit product" }))] }), _jsx("td", { className: "py-2", children: r.tradeItemKey ? (_jsx(Badge, { variant: r.confidence >= 0.8
224
+ ? "default"
225
+ : r.confidence >= 0.5
226
+ ? "secondary"
227
+ : "outline", children: r.confidence >= 0.8 ? "Hoog" : r.confidence >= 0.5 ? "Middel" : "Laag" })) : (_jsx(Badge, { variant: "outline", children: "Geen match" })) }), _jsx("td", { className: "py-2", children: product?.hasSticker ? (_jsxs("label", { className: "flex items-center gap-2 text-xs", children: [_jsx("input", { type: "checkbox", checked: r.replaceExisting, onChange: (e) => setRows((prev) => prev.map((x) => x.fileId === r.fileId
228
+ ? { ...x, replaceExisting: e.target.checked }
229
+ : x)) }), "Vervangen"] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" })) }), _jsx("td", { className: "py-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => setRows((prev) => prev.filter((x) => x.fileId !== r.fileId)), children: _jsx(Trash2, { className: "h-4 w-4" }) }) })] }, r.fileId));
230
+ }) })] }) })), _jsxs(DialogFooter, { className: "gap-2 sm:justify-between", children: [_jsx("span", { className: "text-xs text-muted-foreground flex items-center gap-1.5", children: phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), rows.filter((r) => r.tradeItemKey).length, " van ", rows.length, " gekoppeld"] })) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "outline", disabled: busy, onClick: () => void close(true), children: "Annuleren" }), (phase === "review" || phase === "saving") && (_jsxs(Button, { type: "button", disabled: busy, onClick: () => void save(), children: [phase === "saving" && _jsx(Loader2, { className: "h-4 w-4 mr-1.5 animate-spin" }), "Bevestigen en opslaan"] }))] })] })] }) })] }));
231
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"CustomerAssortmentTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerAssortmentTab.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,cAAc,GACf,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,+BA8LA"}
1
+ {"version":3,"file":"CustomerAssortmentTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerAssortmentTab.tsx"],"names":[],"mappings":"AA0BA,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,cAAc,GACf,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,+BAmQA"}
@@ -1,13 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
- import { useQuery } from "@tanstack/react-query";
3
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { Card, CardContent, Badge, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
6
6
  import { Search, ImageOff } from "lucide-react";
7
7
  import { listCustomerAssortment } from "../../lib/customer-assortment.functions";
8
+ import { listCustomerStickers } from "../../lib/customer-stickers.functions";
8
9
  import { decodeDeep } from "../../lib/decode-html";
9
10
  import { friendlyError } from "../../lib/errors";
10
11
  import { FloridayPriceImportDialog } from "./FloridayPriceImportDialog";
12
+ import { CustomerStickerCell } from "./CustomerStickerCell";
13
+ import { CustomerStickerFormatDialog } from "./CustomerStickerFormatDialog";
14
+ import { BulkStickerUploadDialog } from "./BulkStickerUploadDialog";
11
15
  function money(value, currency) {
12
16
  if (value == null)
13
17
  return "—";
@@ -20,20 +24,36 @@ function money(value, currency) {
20
24
  }
21
25
  export function CustomerAssortmentTab({ customerKey, organizationId, }) {
22
26
  const list = useServerFn(listCustomerAssortment);
27
+ const listStickers = useServerFn(listCustomerStickers);
28
+ const qc = useQueryClient();
23
29
  const [search, setSearch] = useState("");
24
30
  const [onlyPriced, setOnlyPriced] = useState(false);
31
+ const [onlyWithoutSticker, setOnlyWithoutSticker] = useState(false);
25
32
  const { data, isLoading, error } = useQuery({
26
33
  queryKey: ["customer-assortment", customerKey, organizationId ?? null],
27
34
  queryFn: () => list({ data: { customerKey, organizationId: organizationId ?? null } }).then(decodeDeep),
28
35
  enabled: !!customerKey,
29
36
  staleTime: 60000,
30
37
  });
38
+ const { data: stickerData } = useQuery({
39
+ queryKey: ["customer-stickers", customerKey],
40
+ queryFn: () => listStickers({ data: { customerKey } }),
41
+ enabled: !!customerKey,
42
+ });
43
+ const stickerMap = useMemo(() => {
44
+ const m = new Map();
45
+ for (const s of stickerData?.stickers ?? [])
46
+ m.set(s.trade_item_key, s);
47
+ return m;
48
+ }, [stickerData]);
31
49
  const items = data?.items ?? [];
32
50
  const filtered = useMemo(() => {
33
51
  const q = search.trim().toLowerCase();
34
52
  return items.filter((i) => {
35
53
  if (onlyPriced && i.price == null)
36
54
  return false;
55
+ if (onlyWithoutSticker && stickerMap.has(i.key))
56
+ return false;
37
57
  if (!q)
38
58
  return true;
39
59
  return (i.name.toLowerCase().includes(q) ||
@@ -41,7 +61,7 @@ export function CustomerAssortmentTab({ customerKey, organizationId, }) {
41
61
  (i.vbn_code ?? "").toLowerCase().includes(q) ||
42
62
  (i.botanical_name ?? "").toLowerCase().includes(q));
43
63
  });
44
- }, [items, search, onlyPriced]);
64
+ }, [items, search, onlyPriced, onlyWithoutSticker, stickerMap]);
45
65
  if (isLoading) {
46
66
  return (_jsx(Card, { children: _jsx(CardContent, { className: "py-8 text-sm text-muted-foreground", children: "Assortiment laden\u2026" }) }));
47
67
  }
@@ -55,10 +75,22 @@ export function CustomerAssortmentTab({ customerKey, organizationId, }) {
55
75
  }
56
76
  return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: data?.summary.total ?? 0 }), " artikelen"] }), _jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: data?.summary.priced ?? 0 }), " met prijs"] }), pg ? (_jsxs("span", { children: ["Prijsgroep: ", _jsx("strong", { className: "text-foreground", children: pg.name }), pg.pricing_mode === "calculated" && pg.calc_value
57
77
  ? ` (${pg.calc_type === "surcharge" ? "toeslag" : "korting"} ${pg.calc_value}${pg.calc_value_type === "percentage" ? "%" : ` ${currency}`})`
58
- : ""] })) : (_jsx("span", { className: "text-amber-600", children: "Geen prijsgroep ingesteld voor deze klant" })), data?.summary.last_synced_at && (_jsxs("span", { children: ["laatste sync: ", new Date(data.summary.last_synced_at).toLocaleString("nl-NL")] }))] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op naam, artikelcode of VBN-code\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsx("button", { type: "button", onClick: () => setOnlyPriced((v) => !v), className: "inline-flex items-center rounded-full border px-3 py-1 text-xs transition-colors " +
78
+ : ""] })) : (_jsx("span", { className: "text-amber-600", children: "Geen prijsgroep ingesteld voor deze klant" })), data?.summary.last_synced_at && (_jsxs("span", { children: ["laatste sync: ", new Date(data.summary.last_synced_at).toLocaleString("nl-NL")] }))] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsxs("span", { children: ["Stickerformaat:", " ", _jsx("strong", { className: "text-foreground", children: stickerData?.format
79
+ ? `${stickerData.format.width_mm} × ${stickerData.format.height_mm} mm`
80
+ : "nog niet ingesteld" })] }), _jsx(CustomerStickerFormatDialog, { customerKey: customerKey, format: stickerData?.format ?? null }), _jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: stickerMap.size }), " artikelen met sticker"] }), _jsx(BulkStickerUploadDialog, { customerKey: customerKey, products: items.map((i) => ({
81
+ key: i.key,
82
+ name: i.name,
83
+ code: i.supplier_article_code,
84
+ vbn: i.vbn_code,
85
+ botanical: i.botanical_name,
86
+ hasSticker: stickerMap.has(i.key),
87
+ })), onDone: () => qc.invalidateQueries({ queryKey: ["customer-stickers", customerKey] }) })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op naam, artikelcode of VBN-code\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsx("button", { type: "button", onClick: () => setOnlyPriced((v) => !v), className: "inline-flex items-center rounded-full border px-3 py-1 text-xs transition-colors " +
59
88
  (onlyPriced
60
89
  ? "bg-primary text-primary-foreground border-primary"
61
- : "bg-card text-muted-foreground border-border hover:bg-muted"), children: "Alleen met prijs" }), _jsx("div", { className: "ml-auto", children: _jsx(FloridayPriceImportDialog, { customerKey: customerKey, organizationId: organizationId }) })] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[64px]" }), _jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { children: "Verpakking" }), _jsx(TableHead, { className: "text-right", children: "Basisprijs" }), _jsx(TableHead, { className: "text-right", children: "Prijs voor klant" })] }) }), _jsx(TableBody, { children: filtered.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center py-6 text-muted-foreground text-sm", children: "Geen resultaten voor deze zoekopdracht." }) })) : filtered.map((i) => {
90
+ : "bg-card text-muted-foreground border-border hover:bg-muted"), children: "Alleen met prijs" }), _jsx("button", { type: "button", onClick: () => setOnlyWithoutSticker((v) => !v), className: "inline-flex items-center rounded-full border px-3 py-1 text-xs transition-colors " +
91
+ (onlyWithoutSticker
92
+ ? "bg-primary text-primary-foreground border-primary"
93
+ : "bg-card text-muted-foreground border-border hover:bg-muted"), children: "Alleen zonder sticker" }), _jsx("div", { className: "ml-auto", children: _jsx(FloridayPriceImportDialog, { customerKey: customerKey, organizationId: organizationId }) })] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[64px]" }), _jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { children: "Verpakking" }), _jsx(TableHead, { className: "text-right", children: "Basisprijs" }), _jsx(TableHead, { className: "text-right", children: "Prijs voor klant" }), _jsx(TableHead, { children: "Sticker" })] }) }), _jsx(TableBody, { children: filtered.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "text-center py-6 text-muted-foreground text-sm", children: "Geen resultaten voor deze zoekopdracht." }) })) : filtered.map((i) => {
62
94
  const packing = [
63
95
  i.pieces_per_package != null ? `${i.pieces_per_package} per fust` : null,
64
96
  i.packages_per_layer != null ? `${i.packages_per_layer} per laag` : null,
@@ -69,6 +101,6 @@ export function CustomerAssortmentTab({ customerKey, organizationId, }) {
69
101
  i.vbn_code ? `VBN ${i.vbn_code}` : null,
70
102
  i.botanical_name,
71
103
  i.country_of_origin,
72
- ].filter(Boolean).join(" · ") || "—" })] }), _jsxs(TableCell, { className: "text-sm text-muted-foreground", children: [_jsx("div", { children: i.packaging ?? "—" }), packing.length > 0 && (_jsx("div", { className: "text-xs", children: packing.join(" · ") }))] }), _jsx(TableCell, { className: "text-right whitespace-nowrap tabular-nums text-muted-foreground", children: money(i.base_price, i.currency) }), _jsxs(TableCell, { className: "text-right whitespace-nowrap tabular-nums", children: [_jsx("div", { className: "font-medium", children: money(i.price, i.currency) }), i.base_price_source && (_jsx("div", { className: "text-[10px] text-muted-foreground", children: i.base_price_source }))] })] }, i.key));
104
+ ].filter(Boolean).join(" · ") || "—" })] }), _jsxs(TableCell, { className: "text-sm text-muted-foreground", children: [_jsx("div", { children: i.packaging ?? "—" }), packing.length > 0 && (_jsx("div", { className: "text-xs", children: packing.join(" · ") }))] }), _jsx(TableCell, { className: "text-right whitespace-nowrap tabular-nums text-muted-foreground", children: money(i.base_price, i.currency) }), _jsxs(TableCell, { className: "text-right whitespace-nowrap tabular-nums", children: [_jsx("div", { className: "font-medium", children: money(i.price, i.currency) }), i.base_price_source && (_jsx("div", { className: "text-[10px] text-muted-foreground", children: i.base_price_source }))] }), _jsx(TableCell, { children: _jsx(CustomerStickerCell, { customerKey: customerKey, tradeItemKey: i.key, sticker: stickerMap.get(i.key) ?? null }) })] }, i.key));
73
105
  }) })] }) }) })] }));
74
106
  }
@@ -0,0 +1,7 @@
1
+ import { type CustomerSticker } from "../../lib/customer-stickers.functions";
2
+ export declare function CustomerStickerCell({ customerKey, tradeItemKey, sticker, }: {
3
+ customerKey: string;
4
+ tradeItemKey: string;
5
+ sticker: CustomerSticker | null;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=CustomerStickerCell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerStickerCell.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerStickerCell.tsx"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,mCAAmC,CAAC;AAgB3C,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,YAAY,EACZ,OAAO,GACR,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;CACjC,+BAyKA"}
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@flowselections/core";
6
+ import { Upload, FileText, Trash2, Eye, RefreshCw } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { supabase } from "../../integrations/supabase/client";
9
+ import { ALLOWED_STICKER_MIME, confirmStickerUpload, deleteCustomerSticker, } from "../../lib/customer-stickers.functions";
10
+ import { friendlyError } from "../../lib/errors";
11
+ const BUCKET = "customer-stickers";
12
+ const MAX_SIZE = 10 * 1024 * 1024;
13
+ const ACCEPT = ".pdf,.png,.jpg,.jpeg,.svg";
14
+ function extOf(name) {
15
+ const m = /\.([a-z0-9]+)$/i.exec(name);
16
+ return (m?.[1] ?? "bin").toLowerCase();
17
+ }
18
+ function safeKey(value) {
19
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
20
+ }
21
+ export function CustomerStickerCell({ customerKey, tradeItemKey, sticker, }) {
22
+ const qc = useQueryClient();
23
+ const confirm = useServerFn(confirmStickerUpload);
24
+ const remove = useServerFn(deleteCustomerSticker);
25
+ const inputRef = useRef(null);
26
+ const [open, setOpen] = useState(false);
27
+ const [busy, setBusy] = useState(false);
28
+ const invalidate = () => {
29
+ qc.invalidateQueries({ queryKey: ["customer-stickers", customerKey] });
30
+ };
31
+ const upload = async (file) => {
32
+ if (file.size > MAX_SIZE) {
33
+ toast.error("Het bestand is groter dan 10 MB.");
34
+ return;
35
+ }
36
+ const type = file.type || "";
37
+ if (!ALLOWED_STICKER_MIME.includes(type)) {
38
+ toast.error("Gebruik een PDF, PNG, JPG of SVG.");
39
+ return;
40
+ }
41
+ setBusy(true);
42
+ try {
43
+ const path = `${safeKey(customerKey)}/${safeKey(tradeItemKey)}/${crypto.randomUUID()}.${extOf(file.name)}`;
44
+ const { error } = await supabase.storage.from(BUCKET).upload(path, file, {
45
+ contentType: type,
46
+ upsert: false,
47
+ });
48
+ if (error)
49
+ throw error;
50
+ await confirm({
51
+ data: {
52
+ customerKey,
53
+ tradeItemKey,
54
+ filePath: path,
55
+ fileName: file.name,
56
+ mimeType: type,
57
+ fileSize: file.size,
58
+ },
59
+ });
60
+ invalidate();
61
+ toast.success("Sticker opgeslagen");
62
+ setOpen(false);
63
+ }
64
+ catch (e) {
65
+ toast.error(friendlyError(e, "Uploaden mislukt"));
66
+ }
67
+ finally {
68
+ setBusy(false);
69
+ if (inputRef.current)
70
+ inputRef.current.value = "";
71
+ }
72
+ };
73
+ const del = useMutation({
74
+ mutationFn: () => remove({ data: { customerKey, tradeItemKey } }),
75
+ onSuccess: () => {
76
+ invalidate();
77
+ toast.success("Sticker verwijderd");
78
+ setOpen(false);
79
+ },
80
+ onError: (e) => toast.error(friendlyError(e, "Verwijderen mislukt")),
81
+ });
82
+ const isImage = !!sticker?.mime_type?.startsWith("image/");
83
+ return (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, type: "file", accept: ACCEPT, className: "hidden", onChange: (e) => {
84
+ const f = e.target.files?.[0];
85
+ if (f)
86
+ void upload(f);
87
+ } }), sticker ? (_jsxs("button", { type: "button", onClick: () => setOpen(true), className: "flex items-center gap-2 text-left hover:opacity-80", title: sticker.file_name, children: [isImage && sticker.url ? (_jsx("img", { src: sticker.url, alt: "", className: "h-9 w-9 rounded border object-contain bg-muted" })) : (_jsx("div", { className: "h-9 w-9 rounded border bg-muted flex items-center justify-center", children: _jsx(FileText, { className: "h-4 w-4 text-muted-foreground" }) })), _jsx("span", { className: "text-xs max-w-[120px] truncate", children: sticker.file_name })] })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: busy, onClick: () => inputRef.current?.click(), children: [_jsx(Upload, { className: "h-4 w-4 mr-1.5" }), "Uploaden"] })), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Sticker" }) }), sticker && (_jsxs("div", { className: "space-y-3", children: [isImage && sticker.url ? (_jsx("img", { src: sticker.url, alt: sticker.file_name, className: "max-h-64 w-full rounded border object-contain bg-muted" })) : (_jsxs("div", { className: "rounded border bg-muted p-6 flex items-center justify-center gap-2 text-sm text-muted-foreground", children: [_jsx(FileText, { className: "h-5 w-5" }), sticker.file_name] })), _jsxs("p", { className: "text-xs text-muted-foreground", children: [sticker.file_name, sticker.file_size != null
88
+ ? ` · ${(sticker.file_size / 1024).toFixed(0)} kB`
89
+ : ""] })] })), _jsxs(DialogFooter, { className: "gap-2 sm:justify-between", children: [_jsxs(Button, { type: "button", variant: "destructive", disabled: del.isPending || busy, onClick: () => del.mutate(), children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5" }), "Verwijderen"] }), _jsxs("div", { className: "flex gap-2", children: [sticker?.url && (_jsx(Button, { type: "button", variant: "outline", asChild: true, children: _jsxs("a", { href: sticker.url, target: "_blank", rel: "noreferrer", children: [_jsx(Eye, { className: "h-4 w-4 mr-1.5" }), "Bekijken"] }) })), _jsxs(Button, { type: "button", disabled: busy, onClick: () => inputRef.current?.click(), children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1.5" }), "Vervangen"] })] })] })] }) })] }));
90
+ }
@@ -0,0 +1,7 @@
1
+ import { type StickerFormat } from "../../lib/customer-stickers.functions";
2
+ export declare function CustomerStickerFormatDialog({ customerKey, format, trigger, }: {
3
+ customerKey: string;
4
+ format: StickerFormat | null;
5
+ trigger?: React.ReactNode;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=CustomerStickerFormatDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerStickerFormatDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerStickerFormatDialog.tsx"],"names":[],"mappings":"AAQA,OAAO,EAA8B,KAAK,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAGnG,wBAAgB,2BAA2B,CAAC,EAC1C,WAAW,EACX,MAAM,EACN,OAAO,GACR,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,+BAoFA"}
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, } from "@flowselections/core";
6
+ import { Ruler } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { setCustomerStickerSettings } from "../../lib/customer-stickers.functions";
9
+ import { friendlyError } from "../../lib/errors";
10
+ export function CustomerStickerFormatDialog({ customerKey, format, trigger, }) {
11
+ const qc = useQueryClient();
12
+ const save = useServerFn(setCustomerStickerSettings);
13
+ const [open, setOpen] = useState(false);
14
+ const [width, setWidth] = useState(String(format?.width_mm ?? 100));
15
+ const [height, setHeight] = useState(String(format?.height_mm ?? 150));
16
+ useEffect(() => {
17
+ if (open) {
18
+ setWidth(String(format?.width_mm ?? 100));
19
+ setHeight(String(format?.height_mm ?? 150));
20
+ }
21
+ }, [open, format?.width_mm, format?.height_mm]);
22
+ const mut = useMutation({
23
+ mutationFn: () => save({
24
+ data: { customerKey, widthMm: Number(width), heightMm: Number(height) },
25
+ }),
26
+ onSuccess: () => {
27
+ qc.invalidateQueries({ queryKey: ["customer-stickers", customerKey] });
28
+ qc.invalidateQueries({ queryKey: ["customer-sticker-format", customerKey] });
29
+ toast.success("Stickerformaat opgeslagen");
30
+ setOpen(false);
31
+ },
32
+ onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
33
+ });
34
+ const valid = Number(width) > 0 && Number(height) > 0;
35
+ return (_jsxs(_Fragment, { children: [_jsx("span", { onClick: () => setOpen(true), children: trigger ?? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", children: [_jsx(Ruler, { className: "h-4 w-4 mr-2" }), "Stickerformaat"] })) }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-sm", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Stickerformaat van deze klant" }) }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "sticker-width", children: "Breedte (mm)" }), _jsx(Input, { id: "sticker-width", type: "number", min: "1", step: "0.1", value: width, onChange: (e) => setWidth(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "sticker-height", children: "Hoogte (mm)" }), _jsx(Input, { id: "sticker-height", type: "number", min: "1", step: "0.1", value: height, onChange: (e) => setHeight(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Stickers worden bij het printen passend geschaald naar dit formaat." }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(false), children: "Annuleren" }), _jsx(Button, { type: "button", disabled: !valid || mut.isPending, onClick: () => mut.mutate(), children: "Opslaan" })] })] }) })] }));
36
+ }
@@ -24,6 +24,10 @@ export { CustomerAssortmentTab } from "./components/klanten/CustomerAssortmentTa
24
24
  export { listCustomerAssortment } from "./lib/customer-assortment.functions";
25
25
  export { loadFloridayCatalogPrices, type CatalogPrice } from "./lib/floriday-catalog-prices";
26
26
  export { FloridayPriceImportDialog } from "./components/klanten/FloridayPriceImportDialog";
27
+ export { CustomerStickerCell } from "./components/klanten/CustomerStickerCell";
28
+ export { CustomerStickerFormatDialog } from "./components/klanten/CustomerStickerFormatDialog";
29
+ export { BulkStickerUploadDialog } from "./components/klanten/BulkStickerUploadDialog";
30
+ export { listCustomerStickers, confirmStickerUpload, deleteCustomerSticker, getCustomerStickerSettings, setCustomerStickerSettings, getStickerForPrint, suggestStickerMatches, commitStickerBatch, discardStickerBatch, ALLOWED_STICKER_MIME, type CustomerSticker, type CustomerStickersResult, type StickerFormat, } from "./lib/customer-stickers.functions";
27
31
  export { previewFloridayPriceImport, applyFloridayPriceImport, } from "./lib/floriday-price-import.functions";
28
32
  export { getPriceGroupsForCustomer, setPriceGroupForCustomer, resolveCustomerKeyForLocalCustomer, listPriceGroupItemPrices, setPriceGroupItemPrice, } from "./lib/price-groups-local.functions";
29
33
  export { PriceGroupItemPrices } from "./components/klanten/pricegroups/PriceGroupItemPrices";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,wBAAwB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,KAAK,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gDAAgD,CAAC;AAC3F,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,kCAAkC,EAClC,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,UAgBnC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,wBAAwB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,KAAK,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gDAAgD,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,2BAA2B,EAAE,MAAM,kDAAkD,CAAC;AAC/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,EAC1B,0BAA0B,EAC1B,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,GACnB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,kCAAkC,EAClC,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,UAgBnC,CAAC"}
package/dist-lib/index.js CHANGED
@@ -40,6 +40,10 @@ export { CustomerAssortmentTab } from "./components/klanten/CustomerAssortmentTa
40
40
  export { listCustomerAssortment } from "./lib/customer-assortment.functions";
41
41
  export { loadFloridayCatalogPrices } from "./lib/floriday-catalog-prices";
42
42
  export { FloridayPriceImportDialog } from "./components/klanten/FloridayPriceImportDialog";
43
+ export { CustomerStickerCell } from "./components/klanten/CustomerStickerCell";
44
+ export { CustomerStickerFormatDialog } from "./components/klanten/CustomerStickerFormatDialog";
45
+ export { BulkStickerUploadDialog } from "./components/klanten/BulkStickerUploadDialog";
46
+ export { listCustomerStickers, confirmStickerUpload, deleteCustomerSticker, getCustomerStickerSettings, setCustomerStickerSettings, getStickerForPrint, suggestStickerMatches, commitStickerBatch, discardStickerBatch, ALLOWED_STICKER_MIME, } from "./lib/customer-stickers.functions";
43
47
  export { previewFloridayPriceImport, applyFloridayPriceImport, } from "./lib/floriday-price-import.functions";
44
48
  export { getPriceGroupsForCustomer, setPriceGroupForCustomer, resolveCustomerKeyForLocalCustomer, listPriceGroupItemPrices, setPriceGroupItemPrice, } from "./lib/price-groups-local.functions";
45
49
  export { PriceGroupItemPrices } from "./components/klanten/pricegroups/PriceGroupItemPrices";