@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.
- package/dist-lib/components/klanten/BewerkKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/BewerkKlant.js +21 -1
- package/dist-lib/components/klanten/BulkStickerUploadDialog.d.ts +14 -0
- package/dist-lib/components/klanten/BulkStickerUploadDialog.d.ts.map +1 -0
- package/dist-lib/components/klanten/BulkStickerUploadDialog.js +231 -0
- package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/CustomerAssortmentTab.js +37 -5
- package/dist-lib/components/klanten/CustomerStickerCell.d.ts +7 -0
- package/dist-lib/components/klanten/CustomerStickerCell.d.ts.map +1 -0
- package/dist-lib/components/klanten/CustomerStickerCell.js +90 -0
- package/dist-lib/components/klanten/CustomerStickerFormatDialog.d.ts +7 -0
- package/dist-lib/components/klanten/CustomerStickerFormatDialog.d.ts.map +1 -0
- package/dist-lib/components/klanten/CustomerStickerFormatDialog.js +36 -0
- package/dist-lib/index.d.ts +4 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +4 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +565 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +565 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +565 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +568 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1695 -0
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting.functions.d.ts +1695 -0
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/ai-module.server.d.ts +20 -0
- package/dist-lib/lib/ai-module.server.d.ts.map +1 -0
- package/dist-lib/lib/ai-module.server.js +86 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts +565 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-assortment.functions.d.ts +565 -0
- package/dist-lib/lib/customer-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-notes.functions.d.ts +1130 -0
- package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-revenue.functions.d.ts +565 -0
- package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-stickers.functions.d.ts +244731 -0
- package/dist-lib/lib/customer-stickers.functions.d.ts.map +1 -0
- package/dist-lib/lib/customer-stickers.functions.js +467 -0
- package/dist-lib/lib/customers.functions.d.ts +5085 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/delivery-locations.functions.d.ts +2825 -0
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.d.ts +2260 -0
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-assortment.functions.d.ts +565 -0
- package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-discounts.functions.d.ts +1130 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-price-import.functions.d.ts +1130 -0
- package/dist-lib/lib/floriday-price-import.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-trade-items.functions.d.ts +565 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
- package/dist-lib/lib/local-discounts.functions.d.ts +3956 -1
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-events.functions.d.ts +565 -0
- package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-strategy.functions.d.ts +2260 -0
- package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
- package/dist-lib/lib/overdue-invoices.functions.d.ts +565 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups-local.functions.d.ts +7348 -3
- package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups.functions.d.ts +565 -0
- package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adopt.functions.d.ts +1130 -0
- package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.functions.d.ts +565 -0
- package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"
|
|
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":"
|
|
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
|
|
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("
|
|
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
|
+
}
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -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";
|
package/dist-lib/index.d.ts.map
CHANGED
|
@@ -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";
|