@flowselections/floriday-voorraad 1.0.13 → 1.0.15
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/assets/floriday-logo.png.asset.json +11 -0
- package/dist-lib/components/voorraad/ArtikelWizard.d.ts +5 -0
- package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -0
- package/dist-lib/components/voorraad/ArtikelWizard.js +1229 -0
- package/dist-lib/components/voorraad/EditArtikelPage.d.ts +4 -0
- package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -0
- package/dist-lib/components/voorraad/EditArtikelPage.js +51 -0
- package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts +2 -0
- package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts.map +1 -0
- package/dist-lib/components/voorraad/NieuwArtikelPage.js +5 -0
- package/dist-lib/components/voorraad/StockBulkButtons.d.ts +7 -0
- package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -0
- package/dist-lib/components/voorraad/StockBulkButtons.js +356 -0
- package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +7 -0
- package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -0
- package/dist-lib/components/voorraad/ValidationOverviewTable.js +87 -0
- package/dist-lib/components/voorraad/ValidationPage.d.ts +2 -0
- package/dist-lib/components/voorraad/ValidationPage.d.ts.map +1 -0
- package/dist-lib/components/voorraad/ValidationPage.js +12 -0
- package/dist-lib/components/voorraad/VoorraadDashboard.d.ts +6 -0
- package/dist-lib/components/voorraad/VoorraadDashboard.d.ts.map +1 -0
- package/dist-lib/components/voorraad/VoorraadDashboard.js +42 -0
- package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadPage.js +70 -11
- package/dist-lib/components/voorraad/VoorraadPagination.d.ts +10 -0
- package/dist-lib/components/voorraad/VoorraadPagination.d.ts.map +1 -0
- package/dist-lib/components/voorraad/VoorraadPagination.js +12 -0
- package/dist-lib/components/voorraad/VoorraadStockCard.d.ts +8 -0
- package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -0
- package/dist-lib/components/voorraad/VoorraadStockCard.js +33 -0
- package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadTable.js +16 -5
- package/dist-lib/hooks/useFloriday.d.ts +55 -0
- package/dist-lib/hooks/useFloriday.d.ts.map +1 -0
- package/dist-lib/hooks/useFloriday.js +298 -0
- package/dist-lib/hooks/useLocalTradeItems.d.ts +17 -1
- package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
- package/dist-lib/hooks/useLocalTradeItems.js +54 -10
- package/dist-lib/hooks/useTradeItemSpec.d.ts +42 -0
- package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -0
- package/dist-lib/hooks/useTradeItemSpec.js +448 -0
- package/dist-lib/hooks/useTradeItemStock.d.ts +19 -0
- package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -0
- package/dist-lib/hooks/useTradeItemStock.js +78 -0
- package/dist-lib/hooks/useVbnRegulatoryFeatures.d.ts +3 -0
- package/dist-lib/hooks/useVbnRegulatoryFeatures.d.ts.map +1 -0
- package/dist-lib/hooks/useVbnRegulatoryFeatures.js +19 -0
- package/dist-lib/hooks/useVoorraadData.d.ts +2 -1
- package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
- package/dist-lib/hooks/useVoorraadData.js +39 -11
- package/dist-lib/index.d.ts +4 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +6 -5
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +8346 -3367
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +8346 -3367
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +8346 -3367
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +8460 -3341
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +10 -1
- package/dist-lib/lib/__tests__/floricode-required-features.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/floricode-required-features.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/floricode-required-features.test.js +186 -0
- package/dist-lib/lib/floricode-client.d.ts +15 -1
- package/dist-lib/lib/floricode-client.d.ts.map +1 -1
- package/dist-lib/lib/floricode-client.js +42 -4
- package/dist-lib/lib/floricode-required-features.functions.d.ts +10026 -0
- package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -0
- package/dist-lib/lib/floricode-required-features.functions.js +170 -0
- package/dist-lib/lib/floriday-client.d.ts +36 -8
- package/dist-lib/lib/floriday-client.d.ts.map +1 -1
- package/dist-lib/lib/floriday-client.js +206 -25
- package/dist-lib/lib/floriday-env.d.ts +29 -0
- package/dist-lib/lib/floriday-env.d.ts.map +1 -0
- package/dist-lib/lib/floriday-env.js +42 -0
- package/dist-lib/lib/floriday-env.server.d.ts +15 -0
- package/dist-lib/lib/floriday-env.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-env.server.js +13 -0
- package/dist-lib/lib/floriday-field-sources.d.ts +18 -0
- package/dist-lib/lib/floriday-field-sources.d.ts.map +1 -0
- package/dist-lib/lib/floriday-field-sources.js +45 -0
- package/dist-lib/lib/floriday-gateway-entities.d.ts +44 -0
- package/dist-lib/lib/floriday-gateway-entities.d.ts.map +1 -0
- package/dist-lib/lib/floriday-gateway-entities.js +51 -0
- package/dist-lib/lib/floriday-gateway.functions.d.ts +39999 -0
- package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-gateway.functions.js +68 -0
- package/dist-lib/lib/floriday-gateway.server.d.ts +122 -0
- package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-gateway.server.js +230 -0
- package/dist-lib/lib/floriday-import-validator.d.ts +26 -0
- package/dist-lib/lib/floriday-import-validator.d.ts.map +1 -0
- package/dist-lib/lib/floriday-import-validator.js +41 -0
- package/dist-lib/lib/floriday-payload.d.ts +47 -3
- package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
- package/dist-lib/lib/floriday-payload.js +179 -7
- package/dist-lib/lib/floriday-schema-to-zod.d.ts +9 -0
- package/dist-lib/lib/floriday-schema-to-zod.d.ts.map +1 -0
- package/dist-lib/lib/floriday-schema-to-zod.js +53 -0
- package/dist-lib/lib/floriday-writes.functions.d.ts +19998 -0
- package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-writes.functions.js +74 -0
- package/dist-lib/lib/image-resize.d.ts +43 -0
- package/dist-lib/lib/image-resize.d.ts.map +1 -0
- package/dist-lib/lib/image-resize.js +212 -0
- package/dist-lib/lib/normalize-errors-middleware.d.ts +2 -0
- package/dist-lib/lib/normalize-errors-middleware.d.ts.map +1 -0
- package/dist-lib/lib/normalize-errors-middleware.js +24 -0
- package/dist-lib/lib/wizard-step-fields.d.ts +15 -0
- package/dist-lib/lib/wizard-step-fields.d.ts.map +1 -0
- package/dist-lib/lib/wizard-step-fields.js +51 -0
- package/package.json +35 -14
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
4
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button, Card, CardContent, Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, toast, } from "@flowselections/core";
|
|
5
|
+
import { ArrowLeft, ArrowRight, Check, ChevronDown, ClipboardList, Info, Loader2, Plus, Search, Trash2, Upload, UploadCloud, Users, X, } from "lucide-react";
|
|
6
|
+
import { useFloridayConnection } from "../../hooks/useFloridayConnection";
|
|
7
|
+
import { useFlorydayCustomers } from "../../hooks/useFlorydayCustomers";
|
|
8
|
+
import { floriday } from "../../lib/floriday-client";
|
|
9
|
+
import { listFloricode } from "../../lib/floricode-client";
|
|
10
|
+
import { supabase } from "../../integrations/supabase/client";
|
|
11
|
+
import { buildAddTradeItemPayload, emptyWizardData, floridayCacheToWizardData, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
|
|
12
|
+
import { deleteLocalTradeItem, getLocalTradeItem, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
|
|
13
|
+
import { useTradeItemSpec } from "../../hooks/useTradeItemSpec";
|
|
14
|
+
import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures";
|
|
15
|
+
import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday";
|
|
16
|
+
import floridayLogo from "../../assets/floriday-logo.png.asset.json";
|
|
17
|
+
import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
|
|
18
|
+
const empty = emptyWizardData;
|
|
19
|
+
const STEPS = [
|
|
20
|
+
{ key: "vbn", label: "VBN-code" },
|
|
21
|
+
{ key: "artikel", label: "Artikel" },
|
|
22
|
+
{ key: "paspoort", label: "Plantenpaspoort", onlyForPlants: true },
|
|
23
|
+
{ key: "eigenschappen", label: "Eigenschappen" },
|
|
24
|
+
{ key: "klanten", label: "Klanten" },
|
|
25
|
+
{ key: "fotos", label: "Foto's" },
|
|
26
|
+
{ key: "beladingen", label: "Beladingen" },
|
|
27
|
+
{ key: "verpakking", label: "Aanvullende verpakkingsmaterialen" },
|
|
28
|
+
{ key: "samenstelling", label: "Samenstelling", disabled: true },
|
|
29
|
+
{ key: "beschikbaarheid", label: "Verwachte beschikbaarheid" },
|
|
30
|
+
];
|
|
31
|
+
function visibleSteps(d) {
|
|
32
|
+
return STEPS.filter((s) => {
|
|
33
|
+
const meta = s;
|
|
34
|
+
if (meta.onlyForPlants && d.productGroup === "CUT_FLOWER")
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
|
|
40
|
+
const navigate = useNavigate();
|
|
41
|
+
const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
|
|
42
|
+
const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
|
|
43
|
+
const [step, setStep] = useState(0);
|
|
44
|
+
const [data, setData] = useState(empty);
|
|
45
|
+
const [submitting, setSubmitting] = useState(false);
|
|
46
|
+
const [publishToFloriday, setPublishToFloriday] = useState(true);
|
|
47
|
+
const [draftId, setDraftId] = useState(initialDraftId);
|
|
48
|
+
const [draftLoading, setDraftLoading] = useState(Boolean(initialDraftId));
|
|
49
|
+
const [publishedLoading, setPublishedLoading] = useState(Boolean(tradeItemId));
|
|
50
|
+
const isEditingDraft = Boolean(initialDraftId);
|
|
51
|
+
const isEditingPublished = Boolean(tradeItemId);
|
|
52
|
+
const isEditing = isEditingDraft || isEditingPublished;
|
|
53
|
+
// Load existing draft when editing
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!initialDraftId)
|
|
56
|
+
return;
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
setDraftLoading(true);
|
|
59
|
+
getLocalTradeItem(initialDraftId)
|
|
60
|
+
.then((row) => {
|
|
61
|
+
if (cancelled || !row)
|
|
62
|
+
return;
|
|
63
|
+
setData({ ...empty, ...row.data });
|
|
64
|
+
})
|
|
65
|
+
.catch((e) => toast.error(`Lokaal artikel laden mislukt: ${e.message}`))
|
|
66
|
+
.finally(() => {
|
|
67
|
+
if (!cancelled)
|
|
68
|
+
setDraftLoading(false);
|
|
69
|
+
});
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
};
|
|
73
|
+
}, [initialDraftId]);
|
|
74
|
+
// Load existing published trade item from the shared cache
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!tradeItemId || !activeId)
|
|
77
|
+
return;
|
|
78
|
+
let cancelled = false;
|
|
79
|
+
setPublishedLoading(true);
|
|
80
|
+
(async () => {
|
|
81
|
+
try {
|
|
82
|
+
const { data: row, error } = await supabase
|
|
83
|
+
.from("floriday_trade_items_cache")
|
|
84
|
+
.select("data,floriday_id")
|
|
85
|
+
.eq("connection_id", activeId)
|
|
86
|
+
.eq("floriday_id", tradeItemId)
|
|
87
|
+
.maybeSingle();
|
|
88
|
+
if (cancelled)
|
|
89
|
+
return;
|
|
90
|
+
if (error) {
|
|
91
|
+
toast.error(`Artikel laden mislukt: ${error.message}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!row) {
|
|
95
|
+
toast.error("Artikel niet gevonden in de cache");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setData(floridayCacheToWizardData(row.data));
|
|
99
|
+
setPublishToFloriday(true);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (!cancelled)
|
|
103
|
+
toast.error(`Artikel laden mislukt: ${e.message}`);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (!cancelled)
|
|
107
|
+
setPublishedLoading(false);
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
};
|
|
113
|
+
}, [tradeItemId, activeId]);
|
|
114
|
+
// Floricode data (PascalCase resource namen zoals ze in floricode_records staan)
|
|
115
|
+
const [vbnProducts, setVbnProducts] = useState([]);
|
|
116
|
+
const [featureTypes, setFeatureTypes] = useState([]);
|
|
117
|
+
const [packagingTypes, setPackagingTypes] = useState([]);
|
|
118
|
+
const [loadCarriers, setLoadCarriers] = useState([]);
|
|
119
|
+
const [collectionsList, setCollectionsList] = useState([]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
// Kleine resources volledig laden
|
|
122
|
+
listFloricode("FeatureType", undefined, 300).then(setFeatureTypes).catch(() => { });
|
|
123
|
+
listFloricode("PackagingType", undefined, 100).then(setPackagingTypes).catch(() => { });
|
|
124
|
+
listFloricode("LoadCarrier", undefined, 600).then(setLoadCarriers).catch(() => { });
|
|
125
|
+
// VbnProduct (51k records) wordt server-side per productgroep geladen in StepVbn.
|
|
126
|
+
supabase
|
|
127
|
+
.from("collections")
|
|
128
|
+
.select("name")
|
|
129
|
+
.order("name")
|
|
130
|
+
.then(({ data }) => setCollectionsList((data ?? []).map((r) => r.name)));
|
|
131
|
+
}, []);
|
|
132
|
+
// Schema-gedreven: één HMAC-call levert het generieke trade-item schema
|
|
133
|
+
// + alle benodigde code-lists (vbn-product-group, characteristic, …) in
|
|
134
|
+
// één keer aan. Zie docs/floriday-integratie.md §15.
|
|
135
|
+
const schemaQuery = useTradeItemFormSchema();
|
|
136
|
+
const steps = useMemo(() => visibleSteps(data), [data.productGroup, data.articleType]);
|
|
137
|
+
const regulatoryQuery = useVbnRegulatoryFeatures(data.vbnCode);
|
|
138
|
+
const regulatoryFeatures = useMemo(() => {
|
|
139
|
+
const rows = regulatoryQuery.data?.features;
|
|
140
|
+
if (!rows || rows.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return rows.map((r) => ({
|
|
143
|
+
code: r.code,
|
|
144
|
+
label: r.label,
|
|
145
|
+
regulation: r.regulation,
|
|
146
|
+
values: r.values,
|
|
147
|
+
}));
|
|
148
|
+
}, [regulatoryQuery.data]);
|
|
149
|
+
const spec = useTradeItemSpec(schemaQuery.data, data.vbnCode, data.productGroup, regulatoryFeatures);
|
|
150
|
+
// Set van VBN-product-codes die Floriday tijdens write accepteert.
|
|
151
|
+
// VBN-codes zijn 5- of 6-cijferig en staan in `vbn-product-code` (NIET in
|
|
152
|
+
// `vbn-product-group`, dat bevat 8-cijferige groeps-ID's).
|
|
153
|
+
// Leeg/missing → geen filter (graceful fallback).
|
|
154
|
+
const acceptedVbnCodes = useMemo(() => {
|
|
155
|
+
const list = schemaQuery.data?.codeLists?.["vbn-product-code"] ?? [];
|
|
156
|
+
if (!Array.isArray(list) || !list.length)
|
|
157
|
+
return null;
|
|
158
|
+
const set = new Set();
|
|
159
|
+
for (const e of list) {
|
|
160
|
+
const payload = (e?.payload ?? e?.data ?? {});
|
|
161
|
+
const code = e?.code ?? payload.code ?? payload.vbn_code ?? payload.vbnCode;
|
|
162
|
+
if (code != null)
|
|
163
|
+
set.add(String(code));
|
|
164
|
+
}
|
|
165
|
+
return set.size ? set : null;
|
|
166
|
+
}, [schemaQuery.data]);
|
|
167
|
+
// Bij wissel van VBN: gooi features weg die niet meer bij het schema horen.
|
|
168
|
+
const lastPrunedVbnRef = useRef("");
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!data.vbnCode)
|
|
171
|
+
return;
|
|
172
|
+
if (lastPrunedVbnRef.current === data.vbnCode)
|
|
173
|
+
return;
|
|
174
|
+
if (spec.characteristics.length === 0)
|
|
175
|
+
return; // wachten tot spec geladen is
|
|
176
|
+
lastPrunedVbnRef.current = data.vbnCode;
|
|
177
|
+
const allowedByCode = new Map(spec.characteristics.map((c) => [c.code, c]));
|
|
178
|
+
setData((d) => {
|
|
179
|
+
const next = {};
|
|
180
|
+
for (const [k, v] of Object.entries(d.features)) {
|
|
181
|
+
const c = allowedByCode.get(k);
|
|
182
|
+
if (!c)
|
|
183
|
+
continue; // karakteristiek niet meer relevant
|
|
184
|
+
if (c.allowedValues.length > 0 &&
|
|
185
|
+
!c.allowedValues.some((av) => av.code === v)) {
|
|
186
|
+
continue; // waarde valt buiten Floriday-lijst
|
|
187
|
+
}
|
|
188
|
+
next[k] = v;
|
|
189
|
+
}
|
|
190
|
+
if (Object.keys(next).length === Object.keys(d.features).length &&
|
|
191
|
+
Object.entries(next).every(([k, v]) => d.features[k] === v)) {
|
|
192
|
+
return d;
|
|
193
|
+
}
|
|
194
|
+
return { ...d, features: next };
|
|
195
|
+
});
|
|
196
|
+
}, [data.vbnCode, spec.characteristics]);
|
|
197
|
+
const validateEigenschappen = (d) => {
|
|
198
|
+
const hasRegulatoryLookup = /^\d{5,6}$/.test(String(d.vbnCode ?? "").trim());
|
|
199
|
+
if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
errors: ["Verplichte Floricode-karakteristieken worden nog geladen"],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (hasRegulatoryLookup && regulatoryQuery.isError) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
errors: [
|
|
209
|
+
`Verplichte Floricode-karakteristieken konden niet worden opgehaald: ${regulatoryQuery.error instanceof Error ? regulatoryQuery.error.message : "onbekende fout"}`,
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (d.vbnCode && !spec.canDetermineCharacteristics) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
errors: [
|
|
217
|
+
`Karakteristieken konden niet betrouwbaar worden bepaald: ${spec.characteristicIssue ?? "onbekende schema-response"}`,
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const errors = [];
|
|
222
|
+
for (const c of spec.characteristics) {
|
|
223
|
+
const v = (d.features[c.code] ?? "").trim();
|
|
224
|
+
if (!v) {
|
|
225
|
+
if (c.required)
|
|
226
|
+
errors.push(`${c.label} (${c.code}) is verplicht`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (c.allowedValues.length > 0 &&
|
|
230
|
+
!c.allowedValues.some((av) => av.code === v)) {
|
|
231
|
+
errors.push(`${c.label} (${c.code}): waarde "${v}" valt buiten de door Floriday geaccepteerde lijst`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (errors.length === 0)
|
|
235
|
+
return { ok: true };
|
|
236
|
+
return { ok: false, errors };
|
|
237
|
+
};
|
|
238
|
+
const validateStepLocal = (key, d) => {
|
|
239
|
+
if (key === "eigenschappen")
|
|
240
|
+
return validateEigenschappen(d);
|
|
241
|
+
return validateStep(key, d);
|
|
242
|
+
};
|
|
243
|
+
const update = (key, value) => setData((d) => ({ ...d, [key]: value }));
|
|
244
|
+
// Stap 1 (VBN) is de poort: zodra die geldig is, mag de gebruiker vrij
|
|
245
|
+
// tussen alle stappen springen door erop te klikken.
|
|
246
|
+
const firstStepUnlocked = useMemo(() => steps.length > 0 && validateStepLocal(steps[0].key, data).ok, [steps, data]);
|
|
247
|
+
// Per-stap validatie → bron voor visuele status én publish-gate.
|
|
248
|
+
// "samenstelling" is read-only ("binnenkort beschikbaar") en mag nooit
|
|
249
|
+
// als ongeldig tellen.
|
|
250
|
+
const stepValidity = useMemo(() => steps.map((s) => {
|
|
251
|
+
if (s.key === "samenstelling")
|
|
252
|
+
return true;
|
|
253
|
+
return validateStepLocal(s.key, data).ok;
|
|
254
|
+
}), [steps, data, spec, regulatoryQuery.isLoading, regulatoryQuery.isError, regulatoryQuery.error]);
|
|
255
|
+
const invalidStepCount = stepValidity.filter((v) => !v).length;
|
|
256
|
+
const firstInvalidIndex = stepValidity.findIndex((v) => !v);
|
|
257
|
+
const goTo = (i) => {
|
|
258
|
+
if (isEditing || i <= step || firstStepUnlocked)
|
|
259
|
+
setStep(i);
|
|
260
|
+
};
|
|
261
|
+
const next = () => setStep((s) => Math.min(s + 1, steps.length - 1));
|
|
262
|
+
// Autoscroll: bij wisselen van stap brengen we de open kaart in beeld zodat
|
|
263
|
+
// stappen onderaan (6-9) niet "verborgen" openen.
|
|
264
|
+
const stepRefs = useRef([]);
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const el = stepRefs.current[step];
|
|
267
|
+
if (!el)
|
|
268
|
+
return;
|
|
269
|
+
// Klein uitstel zodat de uitgeklapte content gerenderd is.
|
|
270
|
+
const id = window.setTimeout(() => {
|
|
271
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
272
|
+
}, 50);
|
|
273
|
+
return () => window.clearTimeout(id);
|
|
274
|
+
}, [step]);
|
|
275
|
+
const submit = async () => {
|
|
276
|
+
if (!activeId) {
|
|
277
|
+
toast.error("Geen actieve Floriday-connectie geselecteerd");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Publish-gate: blokkeer zolang het trade-item schema niet succesvol
|
|
281
|
+
// geladen is — anders kunnen verplichte karakteristieken ontbreken.
|
|
282
|
+
const wantsPublish = (isEditingPublished && Boolean(tradeItemId)) ||
|
|
283
|
+
(!isEditingPublished && publishToFloriday);
|
|
284
|
+
if (wantsPublish && !schemaQuery.isSuccess) {
|
|
285
|
+
toast.error("Floriday-schema nog niet geladen — wacht even of probeer opnieuw.");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Strict valideer alle stappen voor verzending
|
|
289
|
+
for (const s of steps) {
|
|
290
|
+
const v = validateStepLocal(s.key, data);
|
|
291
|
+
if (!v.ok) {
|
|
292
|
+
toast.error(`${s.label}: ${v.errors[0]}`);
|
|
293
|
+
const idx = steps.findIndex((x) => x.key === s.key);
|
|
294
|
+
if (idx >= 0)
|
|
295
|
+
setStep(idx);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
setSubmitting(true);
|
|
300
|
+
try {
|
|
301
|
+
if (isEditingPublished && tradeItemId) {
|
|
302
|
+
// PUT vervangt het bestaande trade item in Floriday
|
|
303
|
+
const payload = { ...buildAddTradeItemPayload(data), tradeItemId };
|
|
304
|
+
const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
|
|
305
|
+
if (!res?.attemptId) {
|
|
306
|
+
throw new Error("Gateway gaf geen attemptId terug — wijziging niet bevestigd");
|
|
307
|
+
}
|
|
308
|
+
// Optimistisch: cache-rij meteen bijwerken zodat de tabel niet 5 min wacht
|
|
309
|
+
try {
|
|
310
|
+
await supabase
|
|
311
|
+
.from("floriday_trade_items_cache")
|
|
312
|
+
.update({ data: payload, updated_at: new Date().toISOString() })
|
|
313
|
+
.eq("connection_id", activeId)
|
|
314
|
+
.eq("floriday_id", tradeItemId);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
/* niet kritiek, sync corrigeert binnen 5 min */
|
|
318
|
+
}
|
|
319
|
+
toast.success("Artikel bijgewerkt in Floriday");
|
|
320
|
+
}
|
|
321
|
+
else if (publishToFloriday) {
|
|
322
|
+
const payload = buildAddTradeItemPayload(data);
|
|
323
|
+
const res = await floriday.createTradeItem(activeId, payload);
|
|
324
|
+
if (!res?.attemptId) {
|
|
325
|
+
throw new Error("Gateway gaf geen attemptId terug — aanmaken niet bevestigd");
|
|
326
|
+
}
|
|
327
|
+
// Als dit een lokaal artikel was, ruim de draft-rij op
|
|
328
|
+
if (draftId) {
|
|
329
|
+
try {
|
|
330
|
+
await deleteLocalTradeItem(draftId);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Niet kritiek; rij verdwijnt na volgende reload
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
toast.success("Artikel aangemaakt in Floriday");
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const newId = await upsertLocalTradeItem({
|
|
340
|
+
id: draftId,
|
|
341
|
+
connectionId: activeId,
|
|
342
|
+
data,
|
|
343
|
+
});
|
|
344
|
+
setDraftId(newId);
|
|
345
|
+
toast.success(draftId
|
|
346
|
+
? "Wijzigingen opgeslagen (nog niet gepubliceerd)"
|
|
347
|
+
: "Artikel opgeslagen (nog niet gepubliceerd)");
|
|
348
|
+
}
|
|
349
|
+
navigate({ to: "/voorraad" });
|
|
350
|
+
}
|
|
351
|
+
catch (e) {
|
|
352
|
+
const msg = e.message;
|
|
353
|
+
if (msg.includes("NO_ACTIVE_FLORIDAY_CONNECTION")) {
|
|
354
|
+
toast.error("Geen actieve Floriday-connectie. Ga naar Instellingen → Floriday.");
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
toast.error(`Opslaan mislukt: ${msg}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
setSubmitting(false);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const handleDeleteDraft = async () => {
|
|
365
|
+
if (!draftId)
|
|
366
|
+
return;
|
|
367
|
+
try {
|
|
368
|
+
await deleteLocalTradeItem(draftId);
|
|
369
|
+
toast.success("Lokaal artikel verwijderd");
|
|
370
|
+
navigate({ to: "/voorraad" });
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
toast.error(`Verwijderen mislukt: ${e.message}`);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const vbnValid = validateStepLocal("vbn", data).ok;
|
|
377
|
+
const saveDraft = async () => {
|
|
378
|
+
if (!activeId) {
|
|
379
|
+
toast.error("Geen actieve Floriday-connectie geselecteerd");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (!vbnValid) {
|
|
383
|
+
toast.error("Kies eerst een VBN-code voordat je een concept opslaat");
|
|
384
|
+
setStep(0);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
setSubmitting(true);
|
|
388
|
+
try {
|
|
389
|
+
const newId = await upsertLocalTradeItem({
|
|
390
|
+
id: draftId,
|
|
391
|
+
connectionId: activeId,
|
|
392
|
+
data,
|
|
393
|
+
});
|
|
394
|
+
setDraftId(newId);
|
|
395
|
+
toast.success(draftId ? "Concept bijgewerkt" : "Concept opgeslagen");
|
|
396
|
+
navigate({ to: "/voorraad" });
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
toast.error(`Opslaan mislukt: ${e.message}`);
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
setSubmitting(false);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
if (connLoading || draftLoading || publishedLoading) {
|
|
406
|
+
return (_jsx("main", { className: "p-6", children: _jsxs(Card, { className: "p-6 flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), publishedLoading
|
|
407
|
+
? "Artikel laden..."
|
|
408
|
+
: draftLoading
|
|
409
|
+
? "Lokaal artikel laden..."
|
|
410
|
+
: "Floriday-connectie laden..."] }) }));
|
|
411
|
+
}
|
|
412
|
+
if (!activeConnection) {
|
|
413
|
+
return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-connectie gevonden. Voeg er eerst een toe in Instellingen." }) }));
|
|
414
|
+
}
|
|
415
|
+
return (_jsxs("main", { className: "mx-auto w-full max-w-3xl space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/voorraad", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Terug naar voorraad"] }) }), !isEditingPublished && (_jsxs(Button, { variant: "outline", size: "sm", className: "ml-auto", onClick: saveDraft, disabled: submitting || !vbnValid, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), isEditingDraft ? "Concept bijwerken" : "Opslaan als concept"] })), isEditingDraft && (_jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Lokaal artikel verwijderen?" }), _jsx(AlertDialogDescription, { children: "Dit artikel is nog niet gepubliceerd naar Floriday. Verwijderen is definitief." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteDraft, children: "Verwijderen" })] })] })] }))] }), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo.url, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
|
|
416
|
+
const isOpen = i === step;
|
|
417
|
+
const isValid = stepValidity[i];
|
|
418
|
+
const isVisited = i < step || isEditing;
|
|
419
|
+
// Status: valid → blauwe check; visited+invalid → oranje !; untouched → grijs.
|
|
420
|
+
const status = isValid
|
|
421
|
+
? "valid"
|
|
422
|
+
: isVisited
|
|
423
|
+
? "invalid"
|
|
424
|
+
: "untouched";
|
|
425
|
+
const isLastStep = i === steps.length - 1;
|
|
426
|
+
const navDisabled = !isEditing && i > step && !firstStepUnlocked;
|
|
427
|
+
const summary = summaryFor(s.key, data);
|
|
428
|
+
return (_jsx("li", { ref: (el) => { stepRefs.current[i] = el; }, className: "w-full min-w-0", children: _jsxs(Card, { className: `w-full min-w-0 ${isOpen ? "border-primary" : status === "untouched" ? "opacity-70" : ""}`, children: [_jsxs("button", { type: "button", onClick: () => goTo(i), disabled: navDisabled, className: "grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-6 py-4 text-left disabled:cursor-not-allowed", children: [_jsx("span", { className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium ${isOpen
|
|
429
|
+
? "border-primary text-primary"
|
|
430
|
+
: status === "valid"
|
|
431
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
432
|
+
: status === "invalid"
|
|
433
|
+
? "border-destructive bg-destructive text-destructive-foreground"
|
|
434
|
+
: "border-muted-foreground/30 text-muted-foreground"}`, children: status === "valid" ? (_jsx(Check, { className: "h-4 w-4" })) : status === "invalid" ? ("!") : (i + 1) }), _jsx("span", { className: `min-w-0 truncate font-medium ${status === "untouched" ? "text-muted-foreground" : ""}`, children: s.label }), !isOpen && summary && (_jsx("span", { className: `max-w-xs truncate text-xs ${status === "invalid" ? "text-destructive" : "text-muted-foreground"}`, children: status === "invalid" ? `Onvolledig — ${summary}` : summary }))] }), isOpen && (_jsxs(CardContent, { className: "space-y-4 pt-0", children: [_jsx("div", { "data-wizard-form-content": true, className: "w-full min-w-0", children: _jsx(StepBody, { stepKey: s.key, data: data, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo.url, alt: "Floriday", className: "h-7 w-7 shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Label, { htmlFor: "publish-to-floriday", className: "font-medium cursor-pointer", children: "Publiceren naar Floriday" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: publishToFloriday
|
|
435
|
+
? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
|
|
436
|
+
: "Het artikel wordt alleen lokaal opgeslagen. Je kunt het later vanuit de voorraadlijst alsnog publiceren naar Floriday." })] }), _jsx(Switch, { id: "publish-to-floriday", checked: publishToFloriday, onCheckedChange: setPublishToFloriday })] })), (isLastStep || isEditing) && invalidStepCount > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive flex items-center justify-between gap-3", children: [_jsxs("span", { children: ["Nog ", invalidStepCount, " stap", invalidStepCount === 1 ? "" : "pen", " onvolledig"] }), firstInvalidIndex >= 0 && (_jsxs("button", { type: "button", onClick: () => setStep(firstInvalidIndex), className: "underline font-medium", children: ["Ga naar ", steps[firstInvalidIndex].label] }))] })), _jsx("div", { className: "flex justify-end pt-2", children: isLastStep || isEditing ? (_jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), isEditingPublished
|
|
437
|
+
? "Wijzigingen opslaan"
|
|
438
|
+
: publishToFloriday
|
|
439
|
+
? isEditingDraft
|
|
440
|
+
? "Publiceren naar Floriday"
|
|
441
|
+
: "Aanmaken en publiceren"
|
|
442
|
+
: isEditingDraft
|
|
443
|
+
? "Wijzigingen opslaan"
|
|
444
|
+
: "Artikel aanmaken"] })) : (_jsxs(Button, { onClick: next, disabled: !isValid, children: ["Volgende stap", _jsx(ArrowRight, { className: "ml-2 h-4 w-4" })] })) })] }))] }) }, s.key));
|
|
445
|
+
}) })] }));
|
|
446
|
+
}
|
|
447
|
+
function canAdvance(key, d) {
|
|
448
|
+
return validateStep(key, d).ok;
|
|
449
|
+
}
|
|
450
|
+
function summaryFor(key, d) {
|
|
451
|
+
switch (key) {
|
|
452
|
+
case "vbn":
|
|
453
|
+
if (d.vbnCode && d.vbnName)
|
|
454
|
+
return `${d.vbnCode} – ${d.vbnName}`;
|
|
455
|
+
return [d.vbnCode, d.vbnName].filter(Boolean).join(" – ");
|
|
456
|
+
case "artikel":
|
|
457
|
+
return [d.articleCode, d.name, d.articleType === "CUSTOMER_SPECIFIC" ? "klantspecifiek" : "regulier"]
|
|
458
|
+
.filter(Boolean).join(" • ");
|
|
459
|
+
case "paspoort": {
|
|
460
|
+
const names = (d.botanicalName ?? "").split(";").map((s) => s.trim()).filter(Boolean);
|
|
461
|
+
const countries = (d.countryOfOrigin ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
462
|
+
if (!names.length && !countries.length)
|
|
463
|
+
return "Niet ingevuld";
|
|
464
|
+
const a = names.length ? `${names.length} botanische naam${names.length === 1 ? "" : "/namen"}` : "geen naam";
|
|
465
|
+
const b = countries.length ? `${countries.length} land${countries.length === 1 ? "" : "en"}` : "geen land";
|
|
466
|
+
return `${a} • ${b}`;
|
|
467
|
+
}
|
|
468
|
+
case "eigenschappen": {
|
|
469
|
+
const n = Object.values(d.features).filter(Boolean).length;
|
|
470
|
+
return n ? `${n} eigenschap(pen) ingevuld` : "Niet ingevuld";
|
|
471
|
+
}
|
|
472
|
+
case "klanten":
|
|
473
|
+
return d.customerOrganizationIds.length
|
|
474
|
+
? `${d.customerOrganizationIds.length} klant(en)`
|
|
475
|
+
: "Alle klanten";
|
|
476
|
+
case "fotos":
|
|
477
|
+
return `${d.images.length} foto('s)`;
|
|
478
|
+
case "beladingen":
|
|
479
|
+
return d.packingConfigurations.length
|
|
480
|
+
? `${d.packingConfigurations.length} configuratie(s)`
|
|
481
|
+
: "Geen configuratie";
|
|
482
|
+
case "verpakking":
|
|
483
|
+
return d.additionalPackaging.length
|
|
484
|
+
? `${d.additionalPackaging.length} item(s)`
|
|
485
|
+
: "Geen extra verpakking";
|
|
486
|
+
case "samenstelling":
|
|
487
|
+
return "Binnenkort beschikbaar";
|
|
488
|
+
case "beschikbaarheid":
|
|
489
|
+
return d.yearRound ? "Jaarrond" : `${d.weeks.length} week/weken`;
|
|
490
|
+
default:
|
|
491
|
+
return "";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function labelForGroup(g) {
|
|
495
|
+
return g === "CUT_FLOWER" ? "Snijbloem" : g === "POT_PLANT" ? "Kamerplant" : "Tuinplant";
|
|
496
|
+
}
|
|
497
|
+
function StepBody(p) {
|
|
498
|
+
switch (p.stepKey) {
|
|
499
|
+
case "vbn":
|
|
500
|
+
return _jsx(StepVbn, { ...p });
|
|
501
|
+
case "artikel":
|
|
502
|
+
return _jsx(StepArtikel, { ...p });
|
|
503
|
+
case "paspoort":
|
|
504
|
+
return _jsx(StepPaspoort, { ...p });
|
|
505
|
+
case "eigenschappen":
|
|
506
|
+
return _jsx(StepEigenschappen, { ...p });
|
|
507
|
+
case "klanten":
|
|
508
|
+
return _jsx(StepKlanten, { ...p });
|
|
509
|
+
case "fotos":
|
|
510
|
+
return _jsx(StepFotos, { ...p });
|
|
511
|
+
case "beladingen":
|
|
512
|
+
return _jsx(StepBeladingen, { ...p });
|
|
513
|
+
case "verpakking":
|
|
514
|
+
return _jsx(StepVerpakking, { ...p });
|
|
515
|
+
case "samenstelling":
|
|
516
|
+
return (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground", children: "Binnenkort beschikbaar" }));
|
|
517
|
+
case "beschikbaarheid":
|
|
518
|
+
return _jsx(StepBeschikbaarheid, { ...p });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ============================================================
|
|
522
|
+
// Steps
|
|
523
|
+
// ============================================================
|
|
524
|
+
const RECENT_VBN_KEY = "wizard.recentVbn.v1";
|
|
525
|
+
function readRecentVbn() {
|
|
526
|
+
try {
|
|
527
|
+
const raw = localStorage.getItem(RECENT_VBN_KEY);
|
|
528
|
+
return raw ? JSON.parse(raw) : [];
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function pushRecentVbn(item) {
|
|
535
|
+
const list = readRecentVbn().filter((r) => r.code !== item.code);
|
|
536
|
+
list.unshift(item);
|
|
537
|
+
localStorage.setItem(RECENT_VBN_KEY, JSON.stringify(list.slice(0, 6)));
|
|
538
|
+
}
|
|
539
|
+
function groupOfRecord(r) {
|
|
540
|
+
// Floricode VbnProduct gebruikt numeriek product_group_id (bv. 10100202).
|
|
541
|
+
// Eerste twee cijfers coderen de hoofdgroep:
|
|
542
|
+
// 10 = Snijbloem, 20 = Kamerplant, 30 = Tuinplant.
|
|
543
|
+
const raw = r.data?.product_group_id ?? r.data?.productGroupId ?? r.data?.productGroup ?? r.data?.group;
|
|
544
|
+
if (raw == null)
|
|
545
|
+
return null;
|
|
546
|
+
const n = Number(raw);
|
|
547
|
+
if (Number.isFinite(n) && n > 0) {
|
|
548
|
+
const s = String(Math.floor(n));
|
|
549
|
+
if (s.startsWith("10"))
|
|
550
|
+
return "CUT_FLOWER";
|
|
551
|
+
if (s.startsWith("20"))
|
|
552
|
+
return "POT_PLANT";
|
|
553
|
+
if (s.startsWith("30"))
|
|
554
|
+
return "GARDEN_PLANT";
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const g = String(raw).toLowerCase();
|
|
558
|
+
if (g.includes("snij") || g.includes("cut"))
|
|
559
|
+
return "CUT_FLOWER";
|
|
560
|
+
if (g.includes("kamer") || g.includes("pot"))
|
|
561
|
+
return "POT_PLANT";
|
|
562
|
+
if (g.includes("tuin") || g.includes("garden"))
|
|
563
|
+
return "GARDEN_PLANT";
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
function labelOf(r) {
|
|
567
|
+
return String(r.data?.name ?? r.data?.description ?? r.record_id);
|
|
568
|
+
}
|
|
569
|
+
function codeOf(r) {
|
|
570
|
+
// PackagingType heeft `id`, LoadCarrier / FeatureValue hebben `code`,
|
|
571
|
+
// FeatureType heeft `id`.
|
|
572
|
+
return String(r.data?.code ?? r.data?.id ?? r.record_id);
|
|
573
|
+
}
|
|
574
|
+
// Floricode `product_group_id` is een 8-cijferige string. De eerste twee
|
|
575
|
+
// cijfers bepalen het hoofdsegment, maar Floricode gebruikt méér dan één
|
|
576
|
+
// bucket per Floriday-productgroep:
|
|
577
|
+
// 10*, 11* → snijbloemen
|
|
578
|
+
// 20*, 21*, 29* → kamerplanten
|
|
579
|
+
// 30*, 31* → tuinplanten
|
|
580
|
+
// Eén prefix per groep liet daardoor ~6.300 VbnProducts onzichtbaar.
|
|
581
|
+
const GROUP_PREFIXES = {
|
|
582
|
+
CUT_FLOWER: ["10", "11"],
|
|
583
|
+
POT_PLANT: ["20", "21", "29"],
|
|
584
|
+
GARDEN_PLANT: ["30", "31"],
|
|
585
|
+
};
|
|
586
|
+
const GROUP_DOT = {
|
|
587
|
+
CUT_FLOWER: "bg-amber-400",
|
|
588
|
+
POT_PLANT: "bg-rose-500",
|
|
589
|
+
GARDEN_PLANT: "bg-emerald-500",
|
|
590
|
+
};
|
|
591
|
+
function StepVbn({ data, update, acceptedVbnCodes }) {
|
|
592
|
+
const groups = [
|
|
593
|
+
{ value: "ALL", label: "– Alle productgroepen –" },
|
|
594
|
+
{ value: "CUT_FLOWER", label: "Snijbloem" },
|
|
595
|
+
{ value: "POT_PLANT", label: "Kamerplant" },
|
|
596
|
+
{ value: "GARDEN_PLANT", label: "Tuinplant" },
|
|
597
|
+
];
|
|
598
|
+
const [recent, setRecent] = useState([]);
|
|
599
|
+
const [open, setOpen] = useState(false);
|
|
600
|
+
const [query, setQuery] = useState("");
|
|
601
|
+
const [results, setResults] = useState([]);
|
|
602
|
+
const [loading, setLoading] = useState(false);
|
|
603
|
+
const cacheRef = useRef(new Map());
|
|
604
|
+
// null in data.productGroup = "Alle productgroepen"
|
|
605
|
+
const groupValue = data.productGroup ?? "ALL";
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
setRecent(readRecentVbn());
|
|
608
|
+
}, []);
|
|
609
|
+
// Laad records voor gekozen groep + zoekterm (server-side prefix-filter)
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
const prefixes = data.productGroup ? GROUP_PREFIXES[data.productGroup] : null;
|
|
612
|
+
const cacheKey = `${prefixes?.join("|") ?? "ALL"}::${query.trim().toLowerCase()}`;
|
|
613
|
+
const cached = cacheRef.current.get(cacheKey);
|
|
614
|
+
if (cached) {
|
|
615
|
+
setResults(cached);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
setLoading(true);
|
|
619
|
+
const t = setTimeout(() => {
|
|
620
|
+
listFloricode("VbnProduct", query || undefined, 1000, {
|
|
621
|
+
...(prefixes
|
|
622
|
+
? { prefixIn: { field: "product_group_id", prefixes } }
|
|
623
|
+
: {}),
|
|
624
|
+
sortField: "record_id",
|
|
625
|
+
sortAscending: true,
|
|
626
|
+
})
|
|
627
|
+
.then((rows) => {
|
|
628
|
+
cacheRef.current.set(cacheKey, rows);
|
|
629
|
+
setResults(rows);
|
|
630
|
+
})
|
|
631
|
+
.catch(() => setResults([]))
|
|
632
|
+
.finally(() => setLoading(false));
|
|
633
|
+
}, query ? 250 : 0);
|
|
634
|
+
return () => clearTimeout(t);
|
|
635
|
+
}, [data.productGroup, query]);
|
|
636
|
+
const selectVbn = (code, name, group) => {
|
|
637
|
+
update("vbnCode", code);
|
|
638
|
+
update("vbnName", name);
|
|
639
|
+
if (group)
|
|
640
|
+
update("productGroup", group);
|
|
641
|
+
pushRecentVbn({ code, name, group });
|
|
642
|
+
setRecent(readRecentVbn());
|
|
643
|
+
setOpen(false);
|
|
644
|
+
setQuery("");
|
|
645
|
+
};
|
|
646
|
+
const triggerLabel = data.vbnName
|
|
647
|
+
? data.vbnName
|
|
648
|
+
: "Selecteer een VBN productcode";
|
|
649
|
+
return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { children: [_jsx(Label, { className: "mb-2 block", children: "Productcode" }), _jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [_jsxs(Select, { value: groupValue, onValueChange: (v) => {
|
|
650
|
+
const next = v === "ALL" ? null : v;
|
|
651
|
+
update("productGroup", next);
|
|
652
|
+
// reset selectie als groep wisselt
|
|
653
|
+
if (data.vbnCode) {
|
|
654
|
+
update("vbnCode", "");
|
|
655
|
+
update("vbnName", "");
|
|
656
|
+
}
|
|
657
|
+
}, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies productgroep" }) }), _jsx(SelectContent, { children: groups.map((g) => (_jsx(SelectItem, { value: g.value, children: g.label }, g.value))) })] }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 text-left text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", children: [_jsx("span", { className: data.vbnName ? "" : "text-muted-foreground", children: triggerLabel }), _jsx(ChevronDown, { className: "h-4 w-4 opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 4, className: "w-[var(--radix-popover-trigger-width)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Search, { className: "h-4 w-4 text-muted-foreground" }), _jsx("input", { autoFocus: true, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Zoek op naam of code\u2026", className: "h-7 w-full border-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground" })] }), _jsxs("div", { className: "max-h-80 overflow-y-auto", children: [loading && (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "Zoeken\u2026" })), (() => {
|
|
658
|
+
// §15.4 — Floriday-codeLists is bron van waarheid voor wat
|
|
659
|
+
// tijdens write geaccepteerd wordt. Floricode levert de
|
|
660
|
+
// verrijking (labels/groep); codes die Floriday niet kent
|
|
661
|
+
// verbergen we.
|
|
662
|
+
const filtered = acceptedVbnCodes && acceptedVbnCodes.size > 0
|
|
663
|
+
? results.filter((r) => acceptedVbnCodes.has(codeOf(r)))
|
|
664
|
+
: results;
|
|
665
|
+
if (!loading && filtered.length === 0) {
|
|
666
|
+
return (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "Geen resultaten." }));
|
|
667
|
+
}
|
|
668
|
+
return !loading && filtered.map((r) => {
|
|
669
|
+
const code = codeOf(r);
|
|
670
|
+
const name = labelOf(r);
|
|
671
|
+
const grp = groupOfRecord(r);
|
|
672
|
+
const active = data.vbnCode === code;
|
|
673
|
+
return (_jsxs("button", { type: "button", onClick: () => selectVbn(code, name, grp), className: `flex w-full items-center gap-3 px-3 py-2 text-left text-sm hover:bg-muted ${active ? "bg-primary/5" : ""}`, children: [_jsx("span", { className: `h-6 w-6 shrink-0 rounded-full ${grp ? GROUP_DOT[grp] : "bg-muted-foreground/30"} opacity-80` }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium", children: name }), _jsxs("div", { className: "font-mono text-xs text-muted-foreground", children: [code, grp ? ` – ${labelForGroup(grp)}` : ""] })] }), active && _jsx(Check, { className: "h-4 w-4 text-primary" })] }, r.record_id));
|
|
674
|
+
});
|
|
675
|
+
})()] })] })] })] })] }), recent.length > 0 && (_jsxs("div", { children: [_jsx(Label, { className: "mb-2 block", children: "Meest gebruikt" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: recent.slice(0, 2).map((r) => {
|
|
676
|
+
const selected = data.vbnCode === r.code;
|
|
677
|
+
return (_jsxs("button", { type: "button", onClick: () => selectVbn(r.code, r.name, r.group), className: `flex items-center gap-3 rounded-md border p-3 text-left transition-colors ${selected ? "border-primary bg-primary/5" : "hover:border-primary/40"}`, children: [_jsx("span", { className: `h-9 w-9 shrink-0 rounded-full ${r.group ? GROUP_DOT[r.group] : "bg-muted-foreground/30"} opacity-80` }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium", children: r.name || r.code }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [r.code, r.group ? ` - ${labelForGroup(r.group)}` : ""] })] })] }, r.code));
|
|
678
|
+
}) })] }))] }));
|
|
679
|
+
}
|
|
680
|
+
function InfoTip({ text }) {
|
|
681
|
+
return (_jsx("span", { title: text, className: "inline-flex cursor-help text-muted-foreground", children: _jsx(Info, { className: "h-3.5 w-3.5" }) }));
|
|
682
|
+
}
|
|
683
|
+
function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
|
|
684
|
+
const [newCollection, setNewCollection] = useState("");
|
|
685
|
+
const [showNewCollection, setShowNewCollection] = useState(false);
|
|
686
|
+
const addCollection = async (name) => {
|
|
687
|
+
const v = name.trim();
|
|
688
|
+
if (!v)
|
|
689
|
+
return;
|
|
690
|
+
if (!data.collections.includes(v)) {
|
|
691
|
+
update("collections", [...data.collections, v]);
|
|
692
|
+
}
|
|
693
|
+
if (!collectionsList.includes(v)) {
|
|
694
|
+
setCollectionsList([...collectionsList, v]);
|
|
695
|
+
const { data: userData } = await supabase.auth.getUser();
|
|
696
|
+
await supabase
|
|
697
|
+
.from("collections")
|
|
698
|
+
.insert({ name: v, created_by: userData.user?.id ?? null });
|
|
699
|
+
}
|
|
700
|
+
setNewCollection("");
|
|
701
|
+
setShowNewCollection(false);
|
|
702
|
+
};
|
|
703
|
+
const removeCollection = (c) => update("collections", data.collections.filter((x) => x !== c));
|
|
704
|
+
const articleTypes = [
|
|
705
|
+
{ value: "REGULAR", label: "Regulier", description: "Beschikbaar voor iedereen", icon: Users },
|
|
706
|
+
{
|
|
707
|
+
value: "CUSTOMER_SPECIFIC",
|
|
708
|
+
label: "Klantspecifiek",
|
|
709
|
+
description: "Beschikbaar voor één of meerdere klanten",
|
|
710
|
+
icon: ClipboardList,
|
|
711
|
+
},
|
|
712
|
+
];
|
|
713
|
+
return (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Artikelcode ", _jsx("span", { className: "text-destructive", children: "*" }), _jsx(InfoTip, { text: "Dit is je eigen referentie naar dit artikel, ook wel bekend als de kweker-artikelcode." })] }), _jsx(Input, { value: data.articleCode, onChange: (e) => update("articleCode", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Naam ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { value: data.name, onChange: (e) => update("name", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["GTIN ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" }), _jsx(InfoTip, { text: "GTIN staat voor 'Global Trade Item Number', een 13-cijferige barcode" })] }), _jsx(Input, { value: data.gtin, onChange: (e) => update("gtin", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Collecties ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" })] }), data.collections.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: data.collections.map((c) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-1 text-xs", children: [c, _jsx("button", { type: "button", onClick: () => removeCollection(c), className: "hover:text-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, c))) })), !showNewCollection ? (_jsxs(Select, { value: "", onValueChange: (v) => {
|
|
714
|
+
if (v === "__new__") {
|
|
715
|
+
setShowNewCollection(true);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (v && !data.collections.includes(v)) {
|
|
719
|
+
update("collections", [...data.collections, v]);
|
|
720
|
+
}
|
|
721
|
+
}, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Geen collectie" }) }), _jsxs(SelectContent, { children: [collectionsList
|
|
722
|
+
.filter((c) => !data.collections.includes(c))
|
|
723
|
+
.map((c) => (_jsx(SelectItem, { value: c, children: c }, c))), _jsx(SelectItem, { value: "__new__", children: "+ Nieuwe collectie\u2026" })] })] })) : (_jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { autoFocus: true, value: newCollection, onChange: (e) => setNewCollection(e.target.value), placeholder: "Naam nieuwe collectie", onKeyDown: (e) => {
|
|
724
|
+
if (e.key === "Enter") {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
addCollection(newCollection);
|
|
727
|
+
}
|
|
728
|
+
else if (e.key === "Escape") {
|
|
729
|
+
setShowNewCollection(false);
|
|
730
|
+
setNewCollection("");
|
|
731
|
+
}
|
|
732
|
+
} }), _jsx(Button, { type: "button", size: "sm", onClick: () => addCollection(newCollection), children: _jsx(Plus, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => {
|
|
733
|
+
setShowNewCollection(false);
|
|
734
|
+
setNewCollection("");
|
|
735
|
+
}, children: "Annuleer" })] }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Type artikel" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: articleTypes.map((t) => {
|
|
736
|
+
const Icon = t.icon;
|
|
737
|
+
const active = data.articleType === t.value;
|
|
738
|
+
return (_jsxs("button", { type: "button", onClick: () => update("articleType", t.value), className: `flex flex-col items-center gap-2 rounded-lg border p-4 text-center transition-colors ${active
|
|
739
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
740
|
+
: "border-border hover:border-primary/50"}`, children: [_jsx("div", { className: "flex w-full items-start", children: _jsx("span", { className: `flex h-4 w-4 items-center justify-center rounded-full border ${active ? "border-primary-foreground" : "border-muted-foreground/50"}`, children: active && (_jsx("span", { className: "h-2 w-2 rounded-full bg-primary-foreground" })) }) }), _jsx(Icon, { className: "h-6 w-6" }), _jsx("div", { className: "font-medium", children: t.label }), _jsx("div", { className: `text-xs ${active ? "text-primary-foreground/80" : "text-muted-foreground"}`, children: t.description })] }, t.value));
|
|
741
|
+
}) })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx(Checkbox, { checked: data.visibleInExplorer, onCheckedChange: (v) => update("visibleInExplorer", v === true), className: "mt-0.5" }), _jsx("span", { children: "Artikel weergeven bij ontdekken in de Explorer voor kopers" })] }), _jsx("p", { className: "pl-6 text-xs text-muted-foreground", children: "Artikel is altijd zichtbaar via de customer API" })] })] }));
|
|
742
|
+
}
|
|
743
|
+
import { COUNTRIES } from "../../lib/countries";
|
|
744
|
+
function StepPaspoort({ data, update }) {
|
|
745
|
+
const [countryOpen, setCountryOpen] = useState(false);
|
|
746
|
+
const [countryQuery, setCountryQuery] = useState("");
|
|
747
|
+
const selectedCountries = (data.countryOfOrigin || "")
|
|
748
|
+
.split(",")
|
|
749
|
+
.map((c) => c.trim().toUpperCase())
|
|
750
|
+
.filter(Boolean);
|
|
751
|
+
const toggleCountry = (code) => {
|
|
752
|
+
const set = new Set(selectedCountries);
|
|
753
|
+
if (set.has(code))
|
|
754
|
+
set.delete(code);
|
|
755
|
+
else
|
|
756
|
+
set.add(code);
|
|
757
|
+
update("countryOfOrigin", Array.from(set).join(","));
|
|
758
|
+
};
|
|
759
|
+
const filteredCountries = COUNTRIES.filter((c) => {
|
|
760
|
+
const q = countryQuery.trim().toLowerCase();
|
|
761
|
+
if (!q)
|
|
762
|
+
return true;
|
|
763
|
+
return c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q);
|
|
764
|
+
});
|
|
765
|
+
const countryLabel = selectedCountries.length === 0
|
|
766
|
+
? "Selecteer één of meerdere landen"
|
|
767
|
+
: selectedCountries
|
|
768
|
+
.map((c) => COUNTRIES.find((o) => o.code === c)?.name ?? c)
|
|
769
|
+
.join(", ");
|
|
770
|
+
const botanicalNames = data.botanicalName.length > 0
|
|
771
|
+
? data.botanicalName.split(";").map((s) => s.trim()).filter(Boolean)
|
|
772
|
+
: [];
|
|
773
|
+
const [botanicalDraft, setBotanicalDraft] = useState("");
|
|
774
|
+
const commitBotanical = (raw) => {
|
|
775
|
+
const value = raw.trim();
|
|
776
|
+
if (!value)
|
|
777
|
+
return;
|
|
778
|
+
if (botanicalNames.some((n) => n.toLowerCase() === value.toLowerCase())) {
|
|
779
|
+
setBotanicalDraft("");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
update("botanicalName", [...botanicalNames, value].join("; "));
|
|
783
|
+
setBotanicalDraft("");
|
|
784
|
+
};
|
|
785
|
+
const removeBotanical = (idx) => {
|
|
786
|
+
update("botanicalName", botanicalNames.filter((_, i) => i !== idx).join("; "));
|
|
787
|
+
};
|
|
788
|
+
return (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: "Botanische naam" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Input, { value: botanicalDraft, onChange: (e) => setBotanicalDraft(e.target.value), onKeyDown: (e) => {
|
|
789
|
+
if (e.key === "Enter") {
|
|
790
|
+
e.preventDefault();
|
|
791
|
+
commitBotanical(botanicalDraft);
|
|
792
|
+
}
|
|
793
|
+
}, placeholder: "", className: "flex-1" }), _jsx("button", { type: "button", "aria-label": "Voeg botanische naam toe", onClick: () => commitBotanical(botanicalDraft), className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-primary text-primary transition hover:bg-primary hover:text-primary-foreground", children: _jsx(Plus, { className: "h-4 w-4" }) })] }), botanicalNames.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: botanicalNames.map((bn, idx) => (_jsxs("span", { className: "inline-flex items-center gap-2 rounded-md border border-input bg-background px-2.5 py-1 text-sm", children: [bn, _jsx("button", { type: "button", "aria-label": `Verwijder ${bn}`, onClick: () => removeBotanical(idx), className: "flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/70 text-background transition hover:bg-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, `${bn}-${idx}`))) }))] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: "Land van oorsprong" }), _jsxs(Popover, { open: countryOpen, onOpenChange: setCountryOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 text-left text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", children: [_jsx("span", { className: selectedCountries.length === 0
|
|
794
|
+
? "text-muted-foreground"
|
|
795
|
+
: "truncate", children: countryLabel }), _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 4, className: "w-[var(--radix-popover-trigger-width)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Search, { className: "h-4 w-4 text-muted-foreground" }), _jsx("input", { autoFocus: true, value: countryQuery, onChange: (e) => setCountryQuery(e.target.value), placeholder: "Zoek land\u2026", className: "h-7 w-full border-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground" })] }), _jsxs("div", { className: "max-h-72 overflow-y-auto py-1", children: [filteredCountries.length === 0 && (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "Geen resultaten." })), filteredCountries.map((c) => {
|
|
796
|
+
const checked = selectedCountries.includes(c.code);
|
|
797
|
+
return (_jsxs("button", { type: "button", onClick: () => toggleCountry(c.code), className: "flex w-full items-center gap-3 px-3 py-2 text-left text-sm hover:bg-muted", children: [_jsx(Checkbox, { checked: checked }), _jsx("span", { className: "flex-1", children: c.name }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: c.code })] }, c.code));
|
|
798
|
+
})] })] })] })] })] }));
|
|
799
|
+
}
|
|
800
|
+
function StepEigenschappen({ data, update, spec, schemaQuery, regulatoryQuery }) {
|
|
801
|
+
if (!data.vbnCode) {
|
|
802
|
+
return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Selecteer eerst een VBN-code bij stap 1 om de bijbehorende eigenschappen te laden." }));
|
|
803
|
+
}
|
|
804
|
+
if (schemaQuery.isLoading) {
|
|
805
|
+
return (_jsxs("p", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Schema wordt geladen\u2026"] }));
|
|
806
|
+
}
|
|
807
|
+
if (schemaQuery.isError || !schemaQuery.data) {
|
|
808
|
+
return (_jsxs("p", { className: "text-sm text-destructive", children: ["Kon het Floriday trade-item schema niet volledig ophalen: ", schemaQuery.error instanceof Error ? schemaQuery.error.message : "onbekende fout"] }));
|
|
809
|
+
}
|
|
810
|
+
const hasRegulatoryLookup = /^\d{5,6}$/.test(String(data.vbnCode ?? "").trim());
|
|
811
|
+
if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
|
|
812
|
+
return (_jsxs("p", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Verplichte Floricode-karakteristieken worden geladen\u2026"] }));
|
|
813
|
+
}
|
|
814
|
+
if (hasRegulatoryLookup && regulatoryQuery.isError) {
|
|
815
|
+
return (_jsxs("p", { className: "text-sm text-destructive", children: ["Kon verplichte Floricode-karakteristieken niet ophalen: ", regulatoryQuery.error instanceof Error ? regulatoryQuery.error.message : "onbekende fout"] }));
|
|
816
|
+
}
|
|
817
|
+
if (spec.characteristics.length === 0) {
|
|
818
|
+
const s = schemaQuery.data ?? {};
|
|
819
|
+
const diag = {
|
|
820
|
+
requiredFields: s.requiredFields ?? [],
|
|
821
|
+
fieldNames: Array.isArray(s.fields) ? s.fields.map((f) => f.name) : [],
|
|
822
|
+
enumKeys: s.enums ? Object.keys(s.enums) : [],
|
|
823
|
+
hasRawSchema: Boolean(s.schema),
|
|
824
|
+
codeListKeys: s.codeLists ? Object.keys(s.codeLists) : [],
|
|
825
|
+
vbnProductGroupCount: Array.isArray(s.codeLists?.["vbn-product-group"])
|
|
826
|
+
? s.codeLists["vbn-product-group"].length
|
|
827
|
+
: 0,
|
|
828
|
+
vbnProductCodeCount: Array.isArray(s.codeLists?.["vbn-product-code"])
|
|
829
|
+
? s.codeLists["vbn-product-code"].length
|
|
830
|
+
: 0,
|
|
831
|
+
vbnCharacteristicCount: Array.isArray(s.codeLists?.["vbn-characteristic"])
|
|
832
|
+
? s.codeLists["vbn-characteristic"].length
|
|
833
|
+
: 0,
|
|
834
|
+
characteristicCount: Array.isArray(s.codeLists?.characteristic)
|
|
835
|
+
? s.codeLists.characteristic.length
|
|
836
|
+
: 0,
|
|
837
|
+
productGroupId: spec.productGroupId,
|
|
838
|
+
canDetermineCharacteristics: spec.canDetermineCharacteristics,
|
|
839
|
+
characteristicIssue: spec.characteristicIssue ?? null,
|
|
840
|
+
};
|
|
841
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-sm text-destructive", children: "Karakteristieken konden niet betrouwbaar worden bepaald vanuit het Floriday-schema en de meegeleverde code-lists." }), spec.characteristicIssue && (_jsx("p", { className: "text-xs text-muted-foreground", children: spec.characteristicIssue })), import.meta.env.DEV && (_jsxs("details", { className: "text-xs text-muted-foreground", children: [_jsx("summary", { className: "cursor-pointer", children: "Schema-diagnostiek (dev only)" }), _jsx("pre", { className: "mt-2 whitespace-pre-wrap rounded bg-muted p-2", children: JSON.stringify(diag, null, 2) })] }))] }));
|
|
842
|
+
}
|
|
843
|
+
return (_jsx(EigenschappenBody, { data: data, update: update, spec: spec }));
|
|
844
|
+
}
|
|
845
|
+
function EigenschappenBody({ data, update, spec, }) {
|
|
846
|
+
const [visibleOptional, setVisibleOptional] = useState(new Set());
|
|
847
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
848
|
+
const [addSearch, setAddSearch] = useState("");
|
|
849
|
+
// Reset zichtbare optionele set bij wisselen van VBN-code.
|
|
850
|
+
useEffect(() => {
|
|
851
|
+
setVisibleOptional(new Set());
|
|
852
|
+
}, [data.vbnCode]);
|
|
853
|
+
const hasValue = (code) => Boolean((data.features[code] ?? "").trim());
|
|
854
|
+
const visibleFields = spec.characteristics.filter((c) => c.required || visibleOptional.has(c.code) || hasValue(c.code));
|
|
855
|
+
const hiddenOptional = spec.characteristics.filter((c) => !c.required && !visibleOptional.has(c.code) && !hasValue(c.code));
|
|
856
|
+
const filteredHidden = hiddenOptional.filter((c) => {
|
|
857
|
+
if (!addSearch.trim())
|
|
858
|
+
return true;
|
|
859
|
+
const q = addSearch.trim().toLowerCase();
|
|
860
|
+
return (c.code.toLowerCase().includes(q) ||
|
|
861
|
+
c.label.toLowerCase().includes(q));
|
|
862
|
+
});
|
|
863
|
+
const showField = (code) => {
|
|
864
|
+
setVisibleOptional((prev) => {
|
|
865
|
+
const next = new Set(prev);
|
|
866
|
+
next.add(code);
|
|
867
|
+
return next;
|
|
868
|
+
});
|
|
869
|
+
setAddOpen(false);
|
|
870
|
+
setAddSearch("");
|
|
871
|
+
};
|
|
872
|
+
const removeField = (code) => {
|
|
873
|
+
setVisibleOptional((prev) => {
|
|
874
|
+
const next = new Set(prev);
|
|
875
|
+
next.delete(code);
|
|
876
|
+
return next;
|
|
877
|
+
});
|
|
878
|
+
const nextFeatures = { ...data.features };
|
|
879
|
+
delete nextFeatures[code];
|
|
880
|
+
update("features", nextFeatures);
|
|
881
|
+
};
|
|
882
|
+
return (_jsxs("div", { className: "grid gap-4", children: [spec.characteristicsAreFallback && (_jsx("p", { className: "rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900", children: "Floriday levert voor deze VBN-code nog geen specifieke verplichte karakteristieken aan. Voeg via \"+ Eigenschap toevoegen\" de karakteristieken toe die van toepassing zijn." })), visibleFields.length === 0 && !spec.characteristicsAreFallback && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn geen verplichte karakteristieken voor deze VBN-code. Voeg optioneel eigenschappen toe via de knop hieronder." })), visibleFields.map((f) => (_jsx(CharacteristicField, { field: f, value: data.features[f.code] || "", onChange: (v) => update("features", { ...data.features, [f.code]: v }), onRemove: f.required ? undefined : () => removeField(f.code) }, f.code))), _jsx("div", { children: _jsxs(Popover, { open: addOpen, onOpenChange: (o) => { setAddOpen(o); if (!o)
|
|
883
|
+
setAddSearch(""); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: hiddenOptional.length === 0, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Eigenschap toevoegen", hiddenOptional.length > 0 && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["(", hiddenOptional.length, ")"] }))] }) }), _jsxs(PopoverContent, { align: "start", className: "w-[380px] p-0", children: [_jsx("div", { className: "border-b p-2", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: addSearch, onChange: (e) => setAddSearch(e.target.value), placeholder: "Zoek op code of naam\u2026", className: "h-8 pl-7 text-sm" })] }) }), _jsx("div", { className: "max-h-64 overflow-y-auto p-1", children: filteredHidden.length === 0 ? (_jsx("p", { className: "p-3 text-xs text-muted-foreground", children: hiddenOptional.length === 0
|
|
884
|
+
? "Alle beschikbare karakteristieken zijn al toegevoegd."
|
|
885
|
+
: "Geen resultaten." })) : (filteredHidden.map((c) => (_jsxs("button", { type: "button", onClick: () => showField(c.code), className: "flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent", children: [_jsx("span", { className: "truncate", children: c.label }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: c.code })] }, c.code)))) })] })] }) })] }));
|
|
886
|
+
}
|
|
887
|
+
function CharacteristicField({ field, value, onChange, onRemove, }) {
|
|
888
|
+
// Primair: VBN-specifieke waardes uit vbn-characteristic-value (via spec).
|
|
889
|
+
// Fallback: /gateway/code-list voor niet-VBN karakteristieken.
|
|
890
|
+
const hasSpecValues = field.allowedValues.length > 0;
|
|
891
|
+
const codeListQuery = useCodeList(hasSpecValues ? null : field.listName);
|
|
892
|
+
const values = hasSpecValues
|
|
893
|
+
? field.allowedValues
|
|
894
|
+
: (codeListQuery.data?.values ?? []).map((v) => ({
|
|
895
|
+
code: v.code,
|
|
896
|
+
label: v.description ?? v.code,
|
|
897
|
+
}));
|
|
898
|
+
const CLEAR = "__clear__";
|
|
899
|
+
const invalidExistingValue = Boolean(value && values.length > 0 && !values.some((v) => v.code === value));
|
|
900
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Label, { children: [field.label, field.required && _jsx("span", { className: "ml-1 text-destructive", children: "*" }), " ", _jsxs("span", { className: "font-mono text-xs text-muted-foreground", children: ["(", field.code, ")"] })] }), onRemove && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0 text-muted-foreground hover:text-destructive", onClick: onRemove, "aria-label": `Verwijder ${field.label}`, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }), !hasSpecValues && codeListQuery.isLoading ? (_jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), "Waardelijst ", field.listName, " wordt geladen\u2026"] })) : values.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Select, { value: value || undefined, onValueChange: (v) => onChange(v === CLEAR ? "" : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer een door Floriday toegestane waarde\u2026" }) }), _jsxs(SelectContent, { children: [value && (_jsx(SelectItem, { value: CLEAR, children: _jsx("span", { className: "text-muted-foreground", children: "\u2014 Wissen \u2014" }) })), values.slice(0, 1000).map((v) => (_jsxs(SelectItem, { value: v.code, children: [v.label, " ", _jsxs("span", { className: "font-mono text-xs text-muted-foreground", children: ["(", v.code, ")"] })] }, v.code)))] })] }), invalidExistingValue && (_jsxs("p", { className: "text-xs text-destructive", children: ["Huidige waarde \"", value, "\" komt niet voor in de door Floriday toegestane lijst. Kies een geldige waarde."] }))] })) : (_jsx("p", { className: "rounded-md border border-dashed border-muted-foreground/30 bg-muted/40 p-2 text-xs text-muted-foreground", children: "Geen toegestane waarden bekend voor deze karakteristiek. Laat leeg \u2014 vrije invoer wordt door Floriday niet geaccepteerd." }))] }));
|
|
901
|
+
}
|
|
902
|
+
function StepKlanten({ data, update, floridayCustomers }) {
|
|
903
|
+
if (floridayCustomers.length === 0) {
|
|
904
|
+
return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen klantorganisaties geladen vanuit Floriday. Controleer de connectie of probeer later opnieuw." }));
|
|
905
|
+
}
|
|
906
|
+
const toggle = (id) => {
|
|
907
|
+
if (data.customerOrganizationIds.includes(id)) {
|
|
908
|
+
update("customerOrganizationIds", data.customerOrganizationIds.filter((c) => c !== id));
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
update("customerOrganizationIds", [...data.customerOrganizationIds, id]);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
return (_jsx("div", { className: "max-h-72 overflow-y-auto rounded-md border divide-y", children: floridayCustomers.map((c) => (_jsxs("label", { className: "flex items-start gap-3 px-3 py-2.5 text-sm hover:bg-muted cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: data.customerOrganizationIds.includes(c.id), onChange: () => toggle(c.id), className: "h-4 w-4 mt-0.5" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium truncate", children: c.name }), _jsx("div", { className: "text-[11px] font-mono text-muted-foreground truncate select-all", children: c.id })] })] }, c.id))) }));
|
|
915
|
+
}
|
|
916
|
+
function StepFotos({ data, update, activeId }) {
|
|
917
|
+
const inputRef = useRef(null);
|
|
918
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
919
|
+
const [uploading, setUploading] = useState(false);
|
|
920
|
+
const fileToBase64 = (file) => new Promise((resolve, reject) => {
|
|
921
|
+
const r = new FileReader();
|
|
922
|
+
r.onerror = () => reject(r.error);
|
|
923
|
+
r.onload = () => {
|
|
924
|
+
const result = String(r.result || "");
|
|
925
|
+
const idx = result.indexOf(",");
|
|
926
|
+
resolve(idx >= 0 ? result.slice(idx + 1) : result);
|
|
927
|
+
};
|
|
928
|
+
r.readAsDataURL(file);
|
|
929
|
+
});
|
|
930
|
+
const onFiles = async (files) => {
|
|
931
|
+
if (!files || files.length === 0)
|
|
932
|
+
return;
|
|
933
|
+
if (!activeId) {
|
|
934
|
+
toast.error("Geen actieve Floriday-connectie – kan foto's niet uploaden");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const fileArr = Array.from(files);
|
|
938
|
+
setUploading(true);
|
|
939
|
+
try {
|
|
940
|
+
const { data: { user }, } = await supabase.auth.getUser();
|
|
941
|
+
if (!user) {
|
|
942
|
+
toast.error("Je bent niet ingelogd – kan geen foto's uploaden");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const { prepareFloridayImage, UnsupportedImageTypeError, ImageTooLargeError, ImageDecodeError, } = await import("../../lib/image-resize");
|
|
946
|
+
const additions = [];
|
|
947
|
+
// Beperkte concurrency (2) voorkomt dat een drop van 10 bestanden de
|
|
948
|
+
// gateway overspoelt en tegelijk in throttling loopt.
|
|
949
|
+
const queue = [...fileArr];
|
|
950
|
+
const CONCURRENCY = 2;
|
|
951
|
+
const worker = async () => {
|
|
952
|
+
while (queue.length) {
|
|
953
|
+
const original = queue.shift();
|
|
954
|
+
if (!original)
|
|
955
|
+
return;
|
|
956
|
+
// 1) Normaliseren + valideren tegen Floriday Media-regels
|
|
957
|
+
// (min 640×640, max 5 MB, JPG/PNG). Zie image-resize.ts.
|
|
958
|
+
let f;
|
|
959
|
+
try {
|
|
960
|
+
f = await prepareFloridayImage(original);
|
|
961
|
+
}
|
|
962
|
+
catch (e) {
|
|
963
|
+
const message = e instanceof UnsupportedImageTypeError ||
|
|
964
|
+
e instanceof ImageTooLargeError ||
|
|
965
|
+
e instanceof ImageDecodeError
|
|
966
|
+
? e.message
|
|
967
|
+
: `${original.name}: kon afbeelding niet voorbereiden.`;
|
|
968
|
+
toast.error(`${original.name} — ${message}`);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
// 2) Preview + archief in Supabase Storage (best-effort).
|
|
972
|
+
const ext = (f.name.includes(".") ? f.name.split(".").pop() : "jpg") || "jpg";
|
|
973
|
+
const path = `${user.id}/${crypto.randomUUID()}.${ext}`;
|
|
974
|
+
const { error: storageErr } = await supabase.storage
|
|
975
|
+
.from("trade-item-photos")
|
|
976
|
+
.upload(path, f, {
|
|
977
|
+
contentType: f.type || "image/jpeg",
|
|
978
|
+
upsert: false,
|
|
979
|
+
});
|
|
980
|
+
if (storageErr) {
|
|
981
|
+
console.warn("[upload] Supabase storage mislukt", f.name, storageErr);
|
|
982
|
+
}
|
|
983
|
+
// 3) Upload naar Floriday met retries (in de client).
|
|
984
|
+
try {
|
|
985
|
+
const res = await floriday.uploadImage(activeId, f, f.type || "image/jpeg", f.name);
|
|
986
|
+
if (!res.url)
|
|
987
|
+
throw new Error("Floriday upload gaf geen URL terug");
|
|
988
|
+
const previewUrl = URL.createObjectURL(f);
|
|
989
|
+
additions.push({
|
|
990
|
+
name: f.name,
|
|
991
|
+
url: res.url,
|
|
992
|
+
previewUrl,
|
|
993
|
+
imageId: res.imageId ?? null,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
catch (e) {
|
|
997
|
+
const msg = e instanceof Error && e.message
|
|
998
|
+
? e.message
|
|
999
|
+
: "Foto kon niet worden geüpload.";
|
|
1000
|
+
console.error("[StepFotos] upload mislukt", { filename: f.name, message: msg, error: e });
|
|
1001
|
+
toast.error(`${f.name} — ${msg}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
|
|
1006
|
+
if (additions.length) {
|
|
1007
|
+
update("images", [...data.images, ...additions]);
|
|
1008
|
+
toast.success(`${additions.length} foto('s) toegevoegd`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (e) {
|
|
1012
|
+
console.error("[upload] exception", e);
|
|
1013
|
+
toast.error(`Upload mislukt: ${e.message}`);
|
|
1014
|
+
}
|
|
1015
|
+
finally {
|
|
1016
|
+
setUploading(false);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { onDragOver: (e) => {
|
|
1020
|
+
e.preventDefault();
|
|
1021
|
+
setIsDragging(true);
|
|
1022
|
+
}, onDragLeave: () => setIsDragging(false), onDrop: (e) => {
|
|
1023
|
+
e.preventDefault();
|
|
1024
|
+
setIsDragging(false);
|
|
1025
|
+
onFiles(e.dataTransfer.files);
|
|
1026
|
+
}, className: `flex items-center gap-6 rounded-md border-2 border-dashed px-8 py-10 transition-colors ${isDragging ? "border-primary bg-primary/5" : "border-border bg-background"}`, children: [_jsxs("div", { className: "relative shrink-0", children: [_jsx(UploadCloud, { className: "h-16 w-16 text-muted-foreground/60", strokeWidth: 1.25 }), _jsx("div", { className: "absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full border border-muted-foreground/40 bg-background", children: _jsx(Plus, { className: "h-3 w-3 text-muted-foreground" }) })] }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-base font-semibold text-foreground", children: uploading ? "Uploaden..." : "Sleep afbeeldingen in dit venster om ze te uploaden" }), _jsxs("div", { className: "mt-1 text-sm text-muted-foreground", children: ["Of kies", " ", _jsx("button", { type: "button", onClick: () => inputRef.current?.click(), className: "text-foreground underline-offset-2 hover:underline", children: "Bestanden selecteren" })] }), _jsx("div", { className: "mt-2 text-xs text-muted-foreground", children: "JPG of PNG \u00B7 min. 640 \u00D7 640 px \u00B7 max. 5 MB per bestand" })] }), _jsx("input", { ref: inputRef, type: "file", multiple: true, accept: "image/png,image/jpeg", className: "hidden", onChange: (e) => {
|
|
1027
|
+
onFiles(e.target.files);
|
|
1028
|
+
e.target.value = "";
|
|
1029
|
+
} })] }), data.images.length > 0 && (_jsx("div", { className: "grid grid-cols-4 gap-2", children: data.images.map((img, idx) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border", children: [_jsx("img", { src: img.previewUrl || img.url, alt: img.name, className: "h-24 w-full object-cover" }), _jsx("button", { type: "button", onClick: () => update("images", data.images.filter((_, i) => i !== idx)), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1 opacity-0 group-hover:opacity-100", children: _jsx(X, { className: "h-3 w-3" }) })] }, img.url))) }))] }));
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Beladingen — Floriday Suppliers API 2026v1
|
|
1033
|
+
*
|
|
1034
|
+
* UI-flow spiegelt Floriday: overzichtstabel met beladingen, per rij een
|
|
1035
|
+
* "primaire" markering, en een modaal (Dialog) voor toevoegen/bewerken met:
|
|
1036
|
+
* 1. Keuze VBN-fust vs. aangesloten verpakkingsleverancier
|
|
1037
|
+
* 2. Ladingdrager + fust + aantallen
|
|
1038
|
+
* 3. Reguliere of klantspecifieke belading (multi-select organisaties)
|
|
1039
|
+
* 4. Optionele artikelfoto in dit fust (upload via Floriday /media)
|
|
1040
|
+
*/
|
|
1041
|
+
function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCustomers, activeId, }) {
|
|
1042
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
1043
|
+
const [editingId, setEditingId] = useState(null);
|
|
1044
|
+
const openNew = () => {
|
|
1045
|
+
setEditingId(null);
|
|
1046
|
+
setDialogOpen(true);
|
|
1047
|
+
};
|
|
1048
|
+
const openEdit = (id) => {
|
|
1049
|
+
setEditingId(id);
|
|
1050
|
+
setDialogOpen(true);
|
|
1051
|
+
};
|
|
1052
|
+
const remove = (id) => {
|
|
1053
|
+
const next = data.packingConfigurations.filter((p) => p.id !== id);
|
|
1054
|
+
// Zorg dat er altijd één primair is
|
|
1055
|
+
if (next.length && !next.some((p) => p.isPrimary)) {
|
|
1056
|
+
next[0] = { ...next[0], isPrimary: true };
|
|
1057
|
+
}
|
|
1058
|
+
update("packingConfigurations", next);
|
|
1059
|
+
};
|
|
1060
|
+
const setPrimary = (id) => {
|
|
1061
|
+
update("packingConfigurations", data.packingConfigurations.map((p) => ({ ...p, isPrimary: p.id === id })));
|
|
1062
|
+
};
|
|
1063
|
+
const upsert = (item) => {
|
|
1064
|
+
const exists = data.packingConfigurations.some((p) => p.id === item.id);
|
|
1065
|
+
let next;
|
|
1066
|
+
if (exists) {
|
|
1067
|
+
next = data.packingConfigurations.map((p) => (p.id === item.id ? item : p));
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
next = [...data.packingConfigurations, item];
|
|
1071
|
+
}
|
|
1072
|
+
if (item.isPrimary) {
|
|
1073
|
+
next = next.map((p) => ({ ...p, isPrimary: p.id === item.id }));
|
|
1074
|
+
}
|
|
1075
|
+
else if (!next.some((p) => p.isPrimary)) {
|
|
1076
|
+
next[0] = { ...next[0], isPrimary: true };
|
|
1077
|
+
}
|
|
1078
|
+
update("packingConfigurations", next);
|
|
1079
|
+
setDialogOpen(false);
|
|
1080
|
+
setEditingId(null);
|
|
1081
|
+
};
|
|
1082
|
+
const editing = editingId
|
|
1083
|
+
? data.packingConfigurations.find((p) => p.id === editingId) ?? null
|
|
1084
|
+
: null;
|
|
1085
|
+
return (_jsxs("div", { className: "space-y-4", children: [data.packingConfigurations.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen beladingsconfiguratie toegevoegd." }) })) : (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 font-medium", children: "Primair" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Fust" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Ladingdrager" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Stuks / laag / drager" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Type" }), _jsx("th", { className: "px-3 py-2" })] }) }), _jsx("tbody", { className: "divide-y", children: data.packingConfigurations.map((p) => {
|
|
1086
|
+
const pkgLabel = packagingTypes.find((r) => codeOf(r) === p.packagingType);
|
|
1087
|
+
const lcLabel = loadCarriers.find((r) => codeOf(r) === p.loadCarrier);
|
|
1088
|
+
return (_jsxs("tr", { className: "hover:bg-muted/30", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "radio", name: "primary-packing", checked: Boolean(p.isPrimary), onChange: () => setPrimary(p.id), "aria-label": "Als primaire belading instellen" }) }), _jsxs("td", { className: "px-3 py-2", children: [_jsx("div", { className: "font-medium", children: pkgLabel ? labelOf(pkgLabel) : p.packagingType || "—" }), p.source === "SUPPLIER" && (_jsx("div", { className: "text-xs text-muted-foreground", children: p.packagingSupplierName ?? p.packagingSupplierId }))] }), _jsx("td", { className: "px-3 py-2", children: lcLabel ? labelOf(lcLabel) : p.loadCarrier || "—" }), _jsxs("td", { className: "px-3 py-2 tabular-nums", children: [p.unitsPerPackage, " / ", p.packagesPerLayer, " /", " ", p.layersPerLoadCarrier] }), _jsx("td", { className: "px-3 py-2", children: p.isCustomerSpecific ? (_jsxs("span", { className: "rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-900", children: ["Klantspecifiek (", p.customerOrganizationIds.length, ")"] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "Regulier" })) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => openEdit(p.id), children: "Bewerk" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, p.id));
|
|
1089
|
+
}) })] }) })), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: openNew, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Belading toevoegen"] }), _jsx(PackingConfigDialog, { open: dialogOpen, onOpenChange: (o) => {
|
|
1090
|
+
setDialogOpen(o);
|
|
1091
|
+
if (!o)
|
|
1092
|
+
setEditingId(null);
|
|
1093
|
+
}, initial: editing, isFirst: data.packingConfigurations.length === 0, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, activeId: activeId, onSave: upsert })] }));
|
|
1094
|
+
}
|
|
1095
|
+
// ---------------------------------------------------------------
|
|
1096
|
+
// Dialog voor toevoegen/bewerken van één beladingsconfiguratie
|
|
1097
|
+
// ---------------------------------------------------------------
|
|
1098
|
+
function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTypes, loadCarriers, floridayCustomers, activeId, onSave, }) {
|
|
1099
|
+
const emptyItem = () => ({
|
|
1100
|
+
id: crypto.randomUUID(),
|
|
1101
|
+
source: "VBN",
|
|
1102
|
+
loadCarrier: "",
|
|
1103
|
+
packagingType: "",
|
|
1104
|
+
unitsPerPackage: 1,
|
|
1105
|
+
packagesPerLayer: 1,
|
|
1106
|
+
layersPerLoadCarrier: 1,
|
|
1107
|
+
isCustomerSpecific: false,
|
|
1108
|
+
customerOrganizationIds: [],
|
|
1109
|
+
isPrimary: isFirst,
|
|
1110
|
+
});
|
|
1111
|
+
const [draft, setDraft] = useState(emptyItem);
|
|
1112
|
+
const [pkgSearch, setPkgSearch] = useState("");
|
|
1113
|
+
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
|
1114
|
+
const photoInputRef = useRef(null);
|
|
1115
|
+
useEffect(() => {
|
|
1116
|
+
if (open) {
|
|
1117
|
+
setDraft(initial ? { ...initial } : emptyItem());
|
|
1118
|
+
setPkgSearch("");
|
|
1119
|
+
}
|
|
1120
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1121
|
+
}, [open, initial]);
|
|
1122
|
+
const patch = (k, v) => setDraft((d) => ({ ...d, [k]: v }));
|
|
1123
|
+
const filteredPkg = useMemo(() => {
|
|
1124
|
+
const q = pkgSearch.trim().toLowerCase();
|
|
1125
|
+
if (!q)
|
|
1126
|
+
return packagingTypes.slice(0, 200);
|
|
1127
|
+
return packagingTypes
|
|
1128
|
+
.filter((r) => labelOf(r).toLowerCase().includes(q) ||
|
|
1129
|
+
codeOf(r).toLowerCase().includes(q))
|
|
1130
|
+
.slice(0, 200);
|
|
1131
|
+
}, [packagingTypes, pkgSearch]);
|
|
1132
|
+
const toggleCustomer = (id) => {
|
|
1133
|
+
const has = draft.customerOrganizationIds.includes(id);
|
|
1134
|
+
patch("customerOrganizationIds", has
|
|
1135
|
+
? draft.customerOrganizationIds.filter((x) => x !== id)
|
|
1136
|
+
: [...draft.customerOrganizationIds, id]);
|
|
1137
|
+
};
|
|
1138
|
+
const canSave = draft.loadCarrier.length > 0 &&
|
|
1139
|
+
draft.packagingType.length > 0 &&
|
|
1140
|
+
(draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
|
|
1141
|
+
(!draft.isCustomerSpecific || draft.customerOrganizationIds.length > 0);
|
|
1142
|
+
const onUploadPhoto = async (file) => {
|
|
1143
|
+
if (!file || !activeId)
|
|
1144
|
+
return;
|
|
1145
|
+
setUploadingPhoto(true);
|
|
1146
|
+
try {
|
|
1147
|
+
const { prepareFloridayImage } = await import("../../lib/image-resize");
|
|
1148
|
+
const prepared = await prepareFloridayImage(file);
|
|
1149
|
+
const res = await floriday.uploadImage(activeId, prepared, prepared.type || "image/jpeg", prepared.name);
|
|
1150
|
+
if (!res.url)
|
|
1151
|
+
throw new Error("Geen URL ontvangen van Floriday");
|
|
1152
|
+
setDraft((d) => ({
|
|
1153
|
+
...d,
|
|
1154
|
+
photoUrl: res.url,
|
|
1155
|
+
photoImageId: res.imageId ?? undefined,
|
|
1156
|
+
photoPreviewUrl: URL.createObjectURL(prepared),
|
|
1157
|
+
}));
|
|
1158
|
+
toast.success("Foto toegevoegd aan belading");
|
|
1159
|
+
}
|
|
1160
|
+
catch (e) {
|
|
1161
|
+
toast.error(e?.message ?? "Foto kon niet worden geüpload");
|
|
1162
|
+
}
|
|
1163
|
+
finally {
|
|
1164
|
+
setUploadingPhoto(false);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: initial ? "Belading bewerken" : "Nieuwe belading" }) }), _jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Fust van" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { type: "button", onClick: () => setDraft((d) => ({
|
|
1168
|
+
...d,
|
|
1169
|
+
source: "VBN",
|
|
1170
|
+
packagingSupplierId: undefined,
|
|
1171
|
+
packagingSupplierName: undefined,
|
|
1172
|
+
})), className: `flex-1 rounded-md border px-3 py-2 text-sm text-left ${draft.source === "VBN"
|
|
1173
|
+
? "border-primary bg-primary/5"
|
|
1174
|
+
: "hover:border-primary/40"}`, children: [_jsx("div", { className: "font-medium", children: "VBN fust" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Standaard veilingfust uit de VBN codelijst." })] }), _jsxs("button", { type: "button", onClick: () => setDraft((d) => ({ ...d, source: "SUPPLIER" })), className: `flex-1 rounded-md border px-3 py-2 text-sm text-left ${draft.source === "SUPPLIER"
|
|
1175
|
+
? "border-primary bg-primary/5"
|
|
1176
|
+
: "hover:border-primary/40"}`, children: [_jsx("div", { className: "font-medium", children: "Verpakkingsleverancier" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Aangesloten leverancier (Modiform, Paardekooper, \u2026)." })] })] })] }), draft.source === "SUPPLIER" && (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Verpakkingsleverancier" }), _jsxs(Select, { value: draft.packagingSupplierId || undefined, onValueChange: (v) => {
|
|
1177
|
+
const sup = PACKAGING_SUPPLIERS.find((s) => s.id === v);
|
|
1178
|
+
setDraft((d) => ({
|
|
1179
|
+
...d,
|
|
1180
|
+
packagingSupplierId: v,
|
|
1181
|
+
packagingSupplierName: sup?.name,
|
|
1182
|
+
}));
|
|
1183
|
+
}, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies leverancier..." }) }), _jsx(SelectContent, { children: PACKAGING_SUPPLIERS.map((s) => (_jsx(SelectItem, { value: s.id, children: s.name }, s.id))) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Materiaal-ID (optioneel)" }), _jsx(Input, { value: draft.packagingMaterialId ?? "", onChange: (e) => patch("packagingMaterialId", e.target.value), placeholder: "Referentie van de leverancier" })] })] })), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: draft.source === "SUPPLIER" ? "Fallback VBN-fust" : "Fusttype" }), _jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: draft.packagingType
|
|
1184
|
+
? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
|
|
1185
|
+
? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
|
|
1186
|
+
: draft.packagingType)
|
|
1187
|
+
: "Selecteer fust..." }), _jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[380px] p-0", align: "start", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { placeholder: "Zoek op naam of code...", value: pkgSearch, onChange: (e) => setPkgSearch(e.target.value) }) }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredPkg.length === 0 ? (_jsx("div", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: "Geen resultaten" })) : (filteredPkg.map((r) => (_jsxs("button", { type: "button", onClick: () => patch("packagingType", codeOf(r)), className: `flex w-full items-start gap-2 px-3 py-2 text-left text-sm hover:bg-muted ${draft.packagingType === codeOf(r) ? "bg-muted" : ""}`, children: [_jsx("span", { className: "font-mono text-xs text-muted-foreground w-14 shrink-0", children: codeOf(r) }), _jsx("span", { className: "truncate", children: labelOf(r) })] }, r.record_id)))) })] })] })] }), _jsx(DropdownOrInput, { label: "Ladingdrager", value: draft.loadCarrier, onChange: (v) => patch("loadCarrier", v), records: loadCarriers, render: (r) => `${labelOf(r)} (${codeOf(r)})`, valueGetter: codeOf, emptyHint: "Geen LoadCarrier-records gesynchroniseerd." })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Stuks per fust" }), _jsx(Input, { type: "number", min: 1, value: draft.unitsPerPackage, onChange: (e) => patch("unitsPerPackage", Number(e.target.value) || 1) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Fusten per laag" }), _jsx(Input, { type: "number", min: 1, value: draft.packagesPerLayer, onChange: (e) => patch("packagesPerLayer", Number(e.target.value) || 1) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Lagen per drager" }), _jsx(Input, { type: "number", min: 1, value: draft.layersPerLoadCarrier, onChange: (e) => patch("layersPerLoadCarrier", Number(e.target.value) || 1) })] })] }), _jsxs("div", { className: "rounded-md border p-3 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Klantspecifieke belading" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Alleen zichtbaar voor geselecteerde klantorganisaties." })] }), _jsx(Switch, { checked: draft.isCustomerSpecific, onCheckedChange: (v) => patch("isCustomerSpecific", Boolean(v)) })] }), draft.isCustomerSpecific && (_jsx("div", { className: "max-h-40 overflow-y-auto rounded border divide-y", children: floridayCustomers.length === 0 ? (_jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "Geen klantorganisaties geladen." })) : (floridayCustomers.map((c) => (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.customerOrganizationIds.includes(c.id), onChange: () => toggleCustomer(c.id) }), _jsx("span", { className: "truncate", children: c.name })] }, c.id)))) }))] }), _jsxs("div", { className: "rounded-md border p-3 space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Foto van dit fust (optioneel)" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "JPG of PNG \u00B7 min. 640 \u00D7 640 px \u00B7 max. 5 MB" })] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: uploadingPhoto || !activeId, onClick: () => photoInputRef.current?.click(), children: uploadingPhoto ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), " Uploaden"] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), " Foto kiezen"] })) }), _jsx("input", { ref: photoInputRef, type: "file", accept: "image/png,image/jpeg", className: "hidden", onChange: (e) => {
|
|
1188
|
+
onUploadPhoto(e.target.files?.[0]);
|
|
1189
|
+
e.target.value = "";
|
|
1190
|
+
} })] }), (draft.photoPreviewUrl || draft.photoUrl) && (_jsxs("div", { className: "relative inline-block", children: [_jsx("img", { src: draft.photoPreviewUrl || draft.photoUrl, alt: "Belading foto", className: "h-24 rounded-md border object-cover" }), _jsx("button", { type: "button", onClick: () => setDraft((d) => ({
|
|
1191
|
+
...d,
|
|
1192
|
+
photoUrl: undefined,
|
|
1193
|
+
photoImageId: undefined,
|
|
1194
|
+
photoPreviewUrl: undefined,
|
|
1195
|
+
})), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1", children: _jsx(X, { className: "h-3 w-3" }) })] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: Boolean(draft.isPrimary), onChange: (e) => patch("isPrimary", e.target.checked) }), "Als primaire belading gebruiken"] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { type: "button", disabled: !canSave, onClick: () => onSave(draft), children: "Opslaan" })] })] }) }));
|
|
1196
|
+
}
|
|
1197
|
+
function StepVerpakking({ data, update }) {
|
|
1198
|
+
const add = () => update("additionalPackaging", [
|
|
1199
|
+
...data.additionalPackaging,
|
|
1200
|
+
{ id: crypto.randomUUID(), code: "", description: "", quantity: 1 },
|
|
1201
|
+
]);
|
|
1202
|
+
const remove = (id) => update("additionalPackaging", data.additionalPackaging.filter((p) => p.id !== id));
|
|
1203
|
+
const edit = (id, patch) => update("additionalPackaging", data.additionalPackaging.map((p) => (p.id === id ? { ...p, ...patch } : p)));
|
|
1204
|
+
return (_jsxs("div", { className: "space-y-3", children: [data.additionalPackaging.map((p) => (_jsxs("div", { className: "grid grid-cols-[1fr_2fr_100px_auto] gap-2 items-end", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Code" }), _jsx(Input, { value: p.code, onChange: (e) => edit(p.id, { code: e.target.value }), placeholder: "bv. HOES" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Omschrijving" }), _jsx(Input, { value: p.description ?? "", onChange: (e) => edit(p.id, { description: e.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Aantal" }), _jsx(Input, { type: "number", min: 1, value: p.quantity, onChange: (e) => edit(p.id, { quantity: Number(e.target.value) }) })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: add, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Verpakkingsmateriaal toevoegen"] })] }));
|
|
1205
|
+
}
|
|
1206
|
+
function StepBeschikbaarheid({ data, update }) {
|
|
1207
|
+
const weeks = Array.from({ length: 52 }, (_, i) => i + 1);
|
|
1208
|
+
const toggleWeek = (w) => {
|
|
1209
|
+
if (data.weeks.includes(w)) {
|
|
1210
|
+
update("weeks", data.weeks.filter((x) => x !== w));
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
update("weeks", [...data.weeks, w].sort((a, b) => a - b));
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", onClick: () => update("yearRound", true), className: `rounded-md border px-3 py-1.5 text-sm ${data.yearRound ? "border-primary bg-primary/5" : "hover:border-primary/40"}`, children: "Jaarrond" }), _jsx("button", { type: "button", onClick: () => update("yearRound", false), className: `rounded-md border px-3 py-1.5 text-sm ${!data.yearRound ? "border-primary bg-primary/5" : "hover:border-primary/40"}`, children: "Specifieke periode" })] }), !data.yearRound && (_jsxs("div", { children: [_jsx(Label, { className: "mb-2 block", children: "Selecteer weken" }), _jsx("div", { className: "grid grid-cols-13 gap-1", style: { gridTemplateColumns: "repeat(13, minmax(0, 1fr))" }, children: weeks.map((w) => (_jsx("button", { type: "button", onClick: () => toggleWeek(w), className: `rounded border px-1 py-1 text-xs ${data.weeks.includes(w)
|
|
1217
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
1218
|
+
: "hover:border-primary/40"}`, children: w }, w))) })] }))] }));
|
|
1219
|
+
}
|
|
1220
|
+
// ============================================================
|
|
1221
|
+
// Shared
|
|
1222
|
+
// ============================================================
|
|
1223
|
+
function DropdownOrInput({ label, value, onChange, records, render, valueGetter, emptyHint, }) {
|
|
1224
|
+
const v = valueGetter ?? render;
|
|
1225
|
+
if (records.length === 0) {
|
|
1226
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsx(Input, { value: value, onChange: (e) => onChange(e.target.value) }), emptyHint && _jsx("p", { className: "text-xs text-muted-foreground", children: emptyHint })] }));
|
|
1227
|
+
}
|
|
1228
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsxs(Select, { value: value || undefined, onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer..." }) }), _jsx(SelectContent, { children: records.slice(0, 300).map((r) => (_jsx(SelectItem, { value: v(r), children: render(r) }, r.record_id))) })] })] }));
|
|
1229
|
+
}
|