@flowselections/floriday-voorraad 1.0.22 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +4 -4
  2. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  3. package/dist-lib/components/voorraad/ArtikelWizard.js +655 -93
  4. package/dist-lib/components/voorraad/FloridayInfoCard.d.ts +12 -0
  5. package/dist-lib/components/voorraad/FloridayInfoCard.d.ts.map +1 -0
  6. package/dist-lib/components/voorraad/FloridayInfoCard.js +85 -0
  7. package/dist-lib/components/voorraad/StepSamenstelling.d.ts +10 -0
  8. package/dist-lib/components/voorraad/StepSamenstelling.d.ts.map +1 -0
  9. package/dist-lib/components/voorraad/StepSamenstelling.js +333 -0
  10. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  11. package/dist-lib/components/voorraad/VoorraadPage.js +14 -3
  12. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  13. package/dist-lib/components/voorraad/VoorraadTable.js +8 -1
  14. package/dist-lib/hooks/useFloridayPriceGroups.d.ts +20 -0
  15. package/dist-lib/hooks/useFloridayPriceGroups.d.ts.map +1 -0
  16. package/dist-lib/hooks/useFloridayPriceGroups.js +55 -0
  17. package/dist-lib/hooks/useFloridayVbnRequired.d.ts +5 -0
  18. package/dist-lib/hooks/useFloridayVbnRequired.d.ts.map +1 -0
  19. package/dist-lib/hooks/useFloridayVbnRequired.js +16 -0
  20. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  21. package/dist-lib/hooks/useLocalTradeItems.js +14 -0
  22. package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -1
  23. package/dist-lib/hooks/useTradeItemSpec.js +48 -1
  24. package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
  25. package/dist-lib/hooks/useVoorraadData.js +36 -5
  26. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2388 -345
  27. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  28. package/dist-lib/integrations/supabase/client.d.ts +2388 -345
  29. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  30. package/dist-lib/integrations/supabase/client.server.d.ts +2388 -345
  31. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  32. package/dist-lib/integrations/supabase/types.d.ts +2437 -343
  33. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  34. package/dist-lib/integrations/supabase/types.js +11 -9
  35. package/dist-lib/lib/centralSecrets.server.d.ts +21 -0
  36. package/dist-lib/lib/centralSecrets.server.d.ts.map +1 -0
  37. package/dist-lib/lib/centralSecrets.server.js +111 -0
  38. package/dist-lib/lib/floricode-required-features.functions.d.ts +2388 -345
  39. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/floriday-client.d.ts +4 -0
  41. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  42. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +2388 -345
  43. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/floriday-gateway.functions.d.ts +10860 -645
  45. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  47. package/dist-lib/lib/floriday-gateway.server.js +34 -9
  48. package/dist-lib/lib/floriday-info.d.ts +54 -0
  49. package/dist-lib/lib/floriday-info.d.ts.map +1 -0
  50. package/dist-lib/lib/floriday-info.js +114 -0
  51. package/dist-lib/lib/floriday-payload.d.ts +21 -3
  52. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  53. package/dist-lib/lib/floriday-payload.js +167 -34
  54. package/dist-lib/lib/floriday-vbn-required.functions.d.ts +27769 -0
  55. package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -0
  56. package/dist-lib/lib/floriday-vbn-required.functions.js +85 -0
  57. package/dist-lib/lib/floriday-warehouse.functions.d.ts +6602 -473
  58. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  59. package/dist-lib/lib/floriday-writes.functions.d.ts +4681 -594
  60. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  61. package/dist-lib/lib/floriday-writes.functions.js +36 -1
  62. package/dist-lib/lib/trade-item-prices.d.ts +43 -0
  63. package/dist-lib/lib/trade-item-prices.d.ts.map +1 -0
  64. package/dist-lib/lib/trade-item-prices.js +229 -0
  65. package/package.json +3 -3
@@ -2,26 +2,34 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Link, useNavigate } from "@tanstack/react-router";
4
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";
5
+ import { ArrowLeft, ArrowRight, Check, ChevronDown, ClipboardList, Info, Loader2, Plus, Search, Trash2, Upload, UploadCloud, Users, X, Star, } from "lucide-react";
6
6
  import { useFloridayConnection } from "../../hooks/useFloridayConnection";
7
7
  import { useFlorydayCustomers } from "../../hooks/useFlorydayCustomers";
8
8
  import { floriday } from "../../lib/floriday-client";
9
- import { listFloricode } from "../../lib/floricode-client";
9
+ import { listFloricode, listFloricodeByField } from "../../lib/floricode-client";
10
10
  import { supabase } from "../../integrations/supabase/client";
11
- import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, buildPrimaryPackingBase, emptyWizardData, floridayCacheToWizardData, isValidGtin13, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
11
+ import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, buildPrimaryPackingBase, emptyWizardData, floridayCacheToWizardData, isValidGtin13, normalizeFloridayProductGroup, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
12
12
  import { AdditionalPackagingDialog } from "./AdditionalPackagingDialog";
13
13
  import { publishCustomerOfferFn } from "../../lib/floriday-customer-offer.functions";
14
14
  import { upsertSupplyLineForTradeItem } from "../../lib/floriday-supply-line";
15
15
  import { deleteLocalTradeItem, getLocalTradeItem, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
16
16
  import { useTradeItemSpec } from "../../hooks/useTradeItemSpec";
17
+ import { isMissingCharacteristicsError } from "../../lib/floriday-writes.functions";
17
18
  import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures";
19
+ import { useFloridayVbnRequired } from "../../hooks/useFloridayVbnRequired";
20
+ import { saveFloridayVbnRequired } from "../../lib/floriday-vbn-required.functions";
21
+ import { useQueryClient } from "@tanstack/react-query";
18
22
  import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday";
23
+ import { useFloridayPriceGroups } from "../../hooks/useFloridayPriceGroups";
24
+ import { readTradeItemPrices, syncTradeItemPrices } from "../../lib/trade-item-prices";
19
25
  const floridayLogo = "/flowselections-assets/Voorraad/floriday-logo.png";
20
26
  import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
21
27
  import { ProductLinkCard } from "../../components/voorraad/ProductLinkCard";
22
28
  import { PriceStatusBadge } from "../../components/voorraad/PriceStatusBadge";
23
29
  import { OrderHistoryCard } from "../../components/voorraad/OrderHistoryCard";
24
30
  import { ArtikelCompositionCard } from "../../components/voorraad/ArtikelCompositionCard";
31
+ import { StepSamenstelling } from "../../components/voorraad/StepSamenstelling";
32
+ import { FloridayInfoCard } from "../../components/voorraad/FloridayInfoCard";
25
33
  import { stockKeyFor } from "../../hooks/useTradeItemStock";
26
34
  const empty = emptyWizardData;
27
35
  const LOAD_CARRIER_LABELS = {
@@ -70,10 +78,23 @@ function warehouseSubLabel(raw) {
70
78
  const city = addr.city ?? "";
71
79
  return [line, city].filter(Boolean).join(", ") || undefined;
72
80
  }
81
+ async function lookupProductGroupForVbn(vbnCode) {
82
+ const code = String(vbnCode ?? "").trim();
83
+ if (!code)
84
+ return null;
85
+ const { data } = await supabase
86
+ .from("floricode_records")
87
+ .select("record_id,resource,data")
88
+ .eq("resource", "VbnProduct")
89
+ .eq("record_id", code)
90
+ .maybeSingle();
91
+ return data ? groupOfRecord(data) : null;
92
+ }
73
93
  export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, } = {}) {
74
94
  const navigate = useNavigate();
75
95
  const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
76
96
  const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
97
+ const { priceGroups } = useFloridayPriceGroups(activeId);
77
98
  const [step, setStep] = useState(0);
78
99
  const [data, setData] = useState(empty);
79
100
  const [submitting, setSubmitting] = useState(false);
@@ -85,10 +106,76 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
85
106
  const [productLoading, setProductLoading] = useState(Boolean(productId));
86
107
  const [syncState, setSyncState] = useState("idle");
87
108
  const [syncError, setSyncError] = useState(null);
109
+ const [productMeta, setProductMeta] = useState({ floridayActive: false, floridayTradeItemId: null, physicalStock: 0 });
110
+ const [deleteAck, setDeleteAck] = useState(false);
111
+ const [deleteOpen, setDeleteOpen] = useState(false);
112
+ // Codes die Floriday's server-side check bij een eerdere publish-poging als
113
+ // "missing" terugmeldde. Wordt uit een gedeelde Supabase-tabel gelezen
114
+ // (`floriday_vbn_required_characteristics`) zodat iedere gebruiker profiteert
115
+ // van elke geleerde eis, en de gebruiker nooit dezelfde 400 twee keer krijgt.
116
+ const queryClient = useQueryClient();
88
117
  const isEditingDraft = Boolean(initialDraftId);
89
118
  const isEditingPublished = Boolean(tradeItemId);
90
119
  const isEditingProduct = Boolean(productId);
91
120
  const isEditing = isEditingDraft || isEditingPublished || isEditingProduct;
121
+ const lastSavedRef = useRef(null);
122
+ // Zolang de post-load hydraties (product/draft/published + prijs- en features-
123
+ // hydratie) nog kunnen vuren, tellen setData-aanroepen niet als user-edits.
124
+ // Voorkomt dat een automatische Floriday-PUT vlak na openen wordt gestart.
125
+ const hydrationSettledRef = useRef(false);
126
+ useEffect(() => {
127
+ lastSavedRef.current = null;
128
+ hydrationSettledRef.current = false;
129
+ }, [productId, tradeItemId, initialDraftId, activeId]);
130
+ // Zodra alle load-fases klaar zijn, geven we hydraties nog één micro-tick de
131
+ // tijd om `setData` te doen; daarna beschouwen we volgende mutaties als edits.
132
+ useEffect(() => {
133
+ if (draftLoading || publishedLoading || productLoading)
134
+ return;
135
+ const t = window.setTimeout(() => {
136
+ hydrationSettledRef.current = true;
137
+ }, 50);
138
+ return () => window.clearTimeout(t);
139
+ }, [draftLoading, publishedLoading, productLoading]);
140
+ // Nieuw artikel: default de Warehouse-select op het voorkeursmagazijn uit de
141
+ // actieve Floriday-connectie. Alleen wanneer de gebruiker nog niets heeft
142
+ // gekozen en het magazijn ook echt in de Floriday warehouses-cache van deze
143
+ // connectie voorkomt (anders zou de push naar Suppliers API 2026v1 een
144
+ // onbekende warehouseId meesturen).
145
+ const isNewArticle = !productId && !tradeItemId && !initialDraftId;
146
+ const preferredWarehouseId = activeConnection?.preferred_warehouse_id ?? null;
147
+ const resolvePreferredWarehouseId = useCallback(async () => {
148
+ if (!activeId || !preferredWarehouseId)
149
+ return null;
150
+ const { data: wh } = await supabase
151
+ .from("floriday_warehouses_cache")
152
+ .select("floriday_id, data")
153
+ .eq("connection_id", activeId)
154
+ .eq("floriday_id", preferredWarehouseId)
155
+ .maybeSingle();
156
+ if (!wh)
157
+ return null;
158
+ const raw = wh.data ?? {};
159
+ if (raw.isExternal === true || raw.isDeleted === true)
160
+ return null;
161
+ return String(wh.floriday_id);
162
+ }, [activeId, preferredWarehouseId]);
163
+ useEffect(() => {
164
+ if (!isNewArticle)
165
+ return;
166
+ let cancelled = false;
167
+ (async () => {
168
+ const preferred = await resolvePreferredWarehouseId();
169
+ if (cancelled || !preferred)
170
+ return;
171
+ setData((prev) => prev.warehouseId && prev.warehouseId.length > 0
172
+ ? prev
173
+ : { ...prev, warehouseId: preferred });
174
+ })();
175
+ return () => {
176
+ cancelled = true;
177
+ };
178
+ }, [isNewArticle, resolvePreferredWarehouseId]);
92
179
  const saveWarehousePreference = useCallback(async (targetTradeItemId, warehouseId) => {
93
180
  if (!activeId || !targetTradeItemId || !warehouseId)
94
181
  return;
@@ -138,6 +225,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
138
225
  .maybeSingle();
139
226
  warehouseId = pref?.warehouse_id ?? warehouseId;
140
227
  }
228
+ if (!warehouseId) {
229
+ warehouseId = (await resolvePreferredWarehouseId()) ?? "";
230
+ }
141
231
  if (cancelled)
142
232
  return;
143
233
  setData({ ...empty, ...row.data, warehouseId });
@@ -151,8 +241,13 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
151
241
  return () => {
152
242
  cancelled = true;
153
243
  };
154
- }, [initialDraftId, activeId]);
244
+ }, [initialDraftId, activeId, resolvePreferredWarehouseId]);
155
245
  // Load existing ERP product directly into the same Floriday field wizard.
246
+ // Wanneer het product al aan een Floriday trade item is gekoppeld, halen we
247
+ // de authoritative Floriday-payload uit `floriday_trade_items_cache` en
248
+ // mergen die over de lokaal opgeslagen `floriday_payload` heen, zodat elke
249
+ // wizardstap (paspoort, kenmerken, foto's, beladingen, warehouse, …) direct
250
+ // is voorgevuld met wat er in Floriday staat.
156
251
  useEffect(() => {
157
252
  if (!productId)
158
253
  return;
@@ -162,7 +257,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
162
257
  try {
163
258
  const { data: product, error } = await supabase
164
259
  .from("products")
165
- .select("id,product,barcode,article_number,quantity,sale_price,image_url,plants_per_crate,crates_per_layer,layers_per_cart,floriday_active,floriday_trade_item_id,floriday_payload")
260
+ .select("id,product,barcode,article_number,quantity,sale_price,vat_rate,vat_code,image_url,plants_per_crate,crates_per_layer,layers_per_cart,floriday_active,floriday_trade_item_id,floriday_payload")
166
261
  .eq("id", productId)
167
262
  .maybeSingle();
168
263
  if (cancelled)
@@ -180,45 +275,160 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
180
275
  const cpl = Number(product.crates_per_layer) || 0;
181
276
  const lpc = Number(product.layers_per_cart) || 0;
182
277
  const payload = (product.floriday_payload ?? {});
278
+ const fid = (product.floriday_trade_item_id ?? null);
279
+ const fActive = Boolean(product.floriday_active);
280
+ // Floriday-cache + warehouse-voorkeur parallel ophalen wanneer we een
281
+ // gekoppeld trade-item hebben. Missing cache blokkeert de wizard niet;
282
+ // dan valt hij terug op wat er lokaal is opgeslagen.
283
+ let cacheData = null;
284
+ let warehousePref = null;
285
+ if (fid && activeId) {
286
+ try {
287
+ const [cacheRes, prefRes] = await Promise.all([
288
+ supabase
289
+ .from("floriday_trade_items_cache")
290
+ .select("data")
291
+ .eq("connection_id", activeId)
292
+ .eq("floriday_id", fid)
293
+ .maybeSingle(),
294
+ supabase
295
+ .from("trade_item_warehouse_preferences")
296
+ .select("warehouse_id")
297
+ .eq("connection_id", activeId)
298
+ .eq("trade_item_id", fid)
299
+ .maybeSingle(),
300
+ ]);
301
+ if (cacheRes.data?.data) {
302
+ cacheData = floridayCacheToWizardData(cacheRes.data.data);
303
+ if (!cacheData.productGroup) {
304
+ const group = await lookupProductGroupForVbn(cacheData.vbnCode);
305
+ if (group)
306
+ cacheData = { ...cacheData, productGroup: group };
307
+ }
308
+ }
309
+ warehousePref = prefRes.data?.warehouse_id ?? null;
310
+ }
311
+ catch (e) {
312
+ // Niet-fataal: wizard laadt door met lokale gegevens.
313
+ console.warn("Floriday-cache laden mislukt", e);
314
+ }
315
+ }
316
+ const cachePackings = cacheData?.packingConfigurations ?? [];
183
317
  const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
184
- const seededPacking = payloadHasPacking
185
- ? payload.packingConfigurations
186
- : ppc > 0 || cpl > 0 || lpc > 0
187
- ? [
188
- {
189
- id: typeof crypto !== "undefined" && "randomUUID" in crypto
190
- ? crypto.randomUUID()
191
- : `pk-${Date.now()}`,
192
- source: "VBN",
193
- loadCarrier: "DANISH_TROLLEY",
194
- packagingType: "",
195
- unitsPerPackage: ppc || 1,
196
- packagesPerLayer: cpl || 1,
197
- layersPerLoadCarrier: lpc || 1,
198
- isPrimary: true,
199
- isCustomerSpecific: false,
200
- customerOrganizationIds: [],
201
- },
202
- ]
203
- : [];
318
+ const seededPacking = cachePackings.length > 0
319
+ ? cachePackings
320
+ : payloadHasPacking
321
+ ? payload.packingConfigurations
322
+ : ppc > 0 || cpl > 0 || lpc > 0
323
+ ? [
324
+ {
325
+ id: typeof crypto !== "undefined" && "randomUUID" in crypto
326
+ ? crypto.randomUUID()
327
+ : `pk-${Date.now()}`,
328
+ source: "VBN",
329
+ loadCarrier: "DANISH_TROLLEY",
330
+ packagingType: "",
331
+ unitsPerPackage: ppc || 1,
332
+ packagesPerLayer: cpl || 1,
333
+ layersPerLoadCarrier: lpc || 1,
334
+ isPrimary: true,
335
+ isCustomerSpecific: false,
336
+ customerOrganizationIds: [],
337
+ },
338
+ ]
339
+ : [];
340
+ const cacheImages = cacheData?.images ?? [];
341
+ const mergedImages = cacheImages.length > 0
342
+ ? cacheImages
343
+ : Array.isArray(payload.images) && payload.images.length > 0
344
+ ? payload.images
345
+ : imageUrl
346
+ ? [{ url: imageUrl, name: product.product ?? undefined, primary: true }]
347
+ : [];
348
+ const normalizedPayloadProductGroup = normalizeFloridayProductGroup(payload.productGroup) ??
349
+ (await lookupProductGroupForVbn(cacheData?.vbnCode ?? payload.vbnCode));
350
+ const cacheWarehouseId = cacheData?.warehouseId && cacheData.warehouseId.length > 0
351
+ ? cacheData.warehouseId
352
+ : "";
353
+ const payloadWarehouseId = payload.warehouseId ?? "";
354
+ const preferredWarehouseFallback = warehousePref || cacheWarehouseId || payloadWarehouseId
355
+ ? null
356
+ : await resolvePreferredWarehouseId();
357
+ // ── Voorraad ophalen VOOR setData zodat `saleStockPieces` de
358
+ // werkelijke voorraad reflecteert (i.p.v. de altijd-0
359
+ // `products.quantity`-kolom, die de wizard anders bij publiceren
360
+ // terug zou schrijven en zo een bestaande voorraad zou overschrijven
361
+ // met 0).
362
+ let physical = 0;
363
+ try {
364
+ const { data: stockRow } = await supabase
365
+ .from("trade_item_stock")
366
+ .select("physical_stock")
367
+ .eq("trade_item_key", `product:${product.id}`)
368
+ .maybeSingle();
369
+ physical = Number(stockRow?.physical_stock ?? 0);
370
+ }
371
+ catch { /* stock niet gevonden = 0 */ }
204
372
  setData({
205
373
  ...empty,
206
374
  ...payload,
207
- name: product.product ?? payload.name ?? "",
208
- articleCode: String(product.article_number ?? payload.articleCode ?? "").trim(),
209
- gtin: validGtin ? barcode : (payload.gtin ?? ""),
375
+ // Floriday-cache is leidend voor alle Floriday-eigen velden. Alleen
376
+ // niet-lege waarden overschrijven, zodat een leeg veld in de cache
377
+ // de lokale fallback niet onnodig wist.
378
+ ...(cacheData
379
+ ? Object.fromEntries(Object.entries(cacheData).filter(([, v]) => {
380
+ if (v === null || v === undefined)
381
+ return false;
382
+ if (typeof v === "string")
383
+ return v.length > 0;
384
+ if (Array.isArray(v))
385
+ return v.length > 0;
386
+ if (typeof v === "object")
387
+ return Object.keys(v).length > 0;
388
+ return true;
389
+ }))
390
+ : {}),
391
+ name: (cacheData?.name && cacheData.name.length > 0
392
+ ? cacheData.name
393
+ : product.product) ?? payload.name ?? "",
394
+ articleCode: (cacheData?.articleCode && cacheData.articleCode.length > 0
395
+ ? cacheData.articleCode
396
+ : String(product.article_number ?? payload.articleCode ?? "")).trim(),
397
+ productGroup: cacheData?.productGroup ??
398
+ normalizedPayloadProductGroup ??
399
+ empty.productGroup,
400
+ gtin: cacheData?.gtin && cacheData.gtin.length > 0
401
+ ? cacheData.gtin
402
+ : validGtin
403
+ ? barcode
404
+ : (payload.gtin ?? ""),
210
405
  salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
211
- saleStockPieces: Number(product.quantity ?? payload.saleStockPieces) || 0,
212
- images: Array.isArray(payload.images) && payload.images.length > 0
213
- ? payload.images
214
- : imageUrl
215
- ? [{ url: imageUrl, name: product.product ?? undefined, primary: true }]
216
- : [],
406
+ // Voorkeur: werkelijke voorraad uit trade_item_stock.
407
+ // Fallback: opgeslagen payload-waarde. `products.quantity` wordt
408
+ // bewust NIET meer gelezen — die staat structureel op 0.
409
+ saleStockPieces: physical > 0
410
+ ? physical
411
+ : Number(payload.saleStockPieces ?? 0) || 0,
412
+ vatRate: product.vat_rate !== null && product.vat_rate !== undefined
413
+ ? Number(product.vat_rate)
414
+ : (payload.vatRate ?? null),
415
+ vatCode: product.vat_code ?? payload.vatCode ?? null,
416
+ warehouseId: warehousePref ||
417
+ cacheWarehouseId ||
418
+ payloadWarehouseId ||
419
+ preferredWarehouseFallback ||
420
+ "",
421
+ images: mergedImages,
217
422
  linkedProductId: product.id,
218
423
  packingConfigurations: seededPacking,
219
424
  });
220
425
  setDraftId(product.id);
221
- setDraftFloridayId((product.floriday_trade_item_id ?? null));
426
+ setDraftFloridayId(fid);
427
+ setProductMeta({
428
+ floridayActive: fActive,
429
+ floridayTradeItemId: fid,
430
+ physicalStock: physical,
431
+ });
222
432
  }
223
433
  catch (e) {
224
434
  if (!cancelled)
@@ -232,7 +442,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
232
442
  return () => {
233
443
  cancelled = true;
234
444
  };
235
- }, [productId]);
445
+ }, [productId, activeId, resolvePreferredWarehouseId]);
236
446
  // Load existing published trade item from the shared cache
237
447
  useEffect(() => {
238
448
  if (!tradeItemId || !activeId)
@@ -264,9 +474,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
264
474
  return;
265
475
  }
266
476
  const wizardData = floridayCacheToWizardData(row.data);
477
+ const productGroup = wizardData.productGroup ??
478
+ (await lookupProductGroupForVbn(wizardData.vbnCode));
479
+ const prefWarehouseId = pref?.warehouse_id ?? "";
480
+ const cachedWarehouseId = wizardData.warehouseId ?? "";
481
+ const preferredWarehouseFallback = prefWarehouseId || cachedWarehouseId ? null : await resolvePreferredWarehouseId();
267
482
  setData({
268
483
  ...wizardData,
269
- warehouseId: pref?.warehouse_id ?? wizardData.warehouseId,
484
+ productGroup,
485
+ warehouseId: prefWarehouseId || cachedWarehouseId || preferredWarehouseFallback || "",
270
486
  });
271
487
  setPublishToFloriday(true);
272
488
  }
@@ -282,7 +498,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
282
498
  return () => {
283
499
  cancelled = true;
284
500
  };
285
- }, [tradeItemId, activeId]);
501
+ }, [tradeItemId, activeId, resolvePreferredWarehouseId]);
286
502
  // Floricode data (PascalCase resource namen zoals ze in floricode_records staan)
287
503
  const [vbnProducts, setVbnProducts] = useState([]);
288
504
  const [featureTypes, setFeatureTypes] = useState([]);
@@ -326,7 +542,45 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
326
542
  values: r.values,
327
543
  }));
328
544
  }, [regulatoryQuery.data]);
329
- const spec = useTradeItemSpec(schemaQuery.data, data.vbnCode, data.productGroup, regulatoryFeatures);
545
+ const specRaw = useTradeItemSpec(schemaQuery.data, data.vbnCode, data.productGroup, regulatoryFeatures);
546
+ // Gedeelde cache uit `floriday_vbn_required_characteristics`: codes die
547
+ // Floriday's server-side check bij een eerdere publish-poging (door welke
548
+ // gebruiker dan ook) als "missing" terugmeldde. Zodra de VBN-code bekend
549
+ // is, halen we de bekende eisen op zodat het formulier ze meteen toont —
550
+ // ook bij een nieuw artikel dat deze gebruiker nog nooit heeft gepubliceerd.
551
+ const vbnRequiredQuery = useFloridayVbnRequired(data.vbnCode);
552
+ const serverRequiredCodes = useMemo(() => vbnRequiredQuery.data?.requiredCodes ?? [], [vbnRequiredQuery.data]);
553
+ // Voeg de server-required codes toe aan spec.characteristics (als optionele
554
+ // codes al bestaan → forceer required; onbekende codes → nieuw toevoegen met
555
+ // label = code en zonder allowedValues). Zo gedraagt de rest van de wizard
556
+ // (validatie + rendering) zich alsof Floriday deze eisen zelf publiceerde.
557
+ const spec = useMemo(() => {
558
+ if (serverRequiredCodes.length === 0)
559
+ return specRaw;
560
+ const byCode = new Map(specRaw.characteristics.map((c) => [c.code, c]));
561
+ for (const code of serverRequiredCodes) {
562
+ const existing = byCode.get(code);
563
+ if (existing) {
564
+ byCode.set(code, { ...existing, required: true });
565
+ }
566
+ else {
567
+ byCode.set(code, {
568
+ code,
569
+ label: code,
570
+ required: true,
571
+ listName: code,
572
+ allowedValues: [],
573
+ });
574
+ }
575
+ }
576
+ const characteristics = Array.from(byCode.values()).sort((a, b) => {
577
+ if (a.required !== b.required)
578
+ return Number(b.required) - Number(a.required);
579
+ return a.code.localeCompare(b.code);
580
+ });
581
+ const requiredCharacteristicCodes = new Set(characteristics.filter((c) => c.required).map((c) => c.code));
582
+ return { ...specRaw, characteristics, requiredCharacteristicCodes };
583
+ }, [specRaw, serverRequiredCodes]);
330
584
  // Set van VBN-product-codes die Floriday tijdens write accepteert.
331
585
  // VBN-codes zijn 5- of 6-cijferig en staan in `vbn-product-code` (NIET in
332
586
  // `vbn-product-group`, dat bevat 8-cijferige groeps-ID's).
@@ -350,7 +604,49 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
350
604
  productId: !tradeItemId ? linkedProductId : null,
351
605
  draftId: !tradeItemId && !linkedProductId ? (draftId ?? null) : null,
352
606
  });
607
+ // Hydrateer basisprijs + prijsgroep-prijzen uit de lokale spiegel wanneer
608
+ // we bewerken en de trade_item_key bekend is.
609
+ const pricesHydratedRef = useRef("");
610
+ useEffect(() => {
611
+ if (!localTradeItemKey)
612
+ return;
613
+ if (pricesHydratedRef.current === localTradeItemKey)
614
+ return;
615
+ if (!isEditing)
616
+ return;
617
+ let cancelled = false;
618
+ pricesHydratedRef.current = localTradeItemKey;
619
+ readTradeItemPrices(localTradeItemKey)
620
+ .then((rows) => {
621
+ if (cancelled || rows.length === 0)
622
+ return;
623
+ setData((d) => {
624
+ const next = { ...d };
625
+ const groupPrices = { ...(d.priceGroupPrices ?? {}) };
626
+ for (const r of rows) {
627
+ if (r.price_group_id) {
628
+ groupPrices[r.price_group_id] = Number(r.price_per_piece);
629
+ }
630
+ else if (!(d.salePricePerPiece > 0)) {
631
+ next.salePricePerPiece = Number(r.price_per_piece);
632
+ }
633
+ }
634
+ next.priceGroupPrices = groupPrices;
635
+ return next;
636
+ });
637
+ })
638
+ .catch(() => {
639
+ pricesHydratedRef.current = "";
640
+ });
641
+ return () => {
642
+ cancelled = true;
643
+ };
644
+ }, [localTradeItemKey, isEditing]);
353
645
  // Bij wissel van VBN: gooi features weg die niet meer bij het schema horen.
646
+ // BELANGRIJK: de eerste keer dat we een vbnCode zien (initiële hydratie vanuit
647
+ // Floriday-cache of lokale draft) mogen we NIETS droppen — anders verdwijnen
648
+ // stil karakteristieken die Floriday wel kent maar ons lokale schema (nog) niet.
649
+ // Alleen bij een échte user-driven VBN-wissel prunen we.
354
650
  const lastPrunedVbnRef = useRef("");
355
651
  useEffect(() => {
356
652
  if (!data.vbnCode)
@@ -359,6 +655,16 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
359
655
  return;
360
656
  if (spec.characteristics.length === 0)
361
657
  return; // wachten tot spec geladen is
658
+ if (spec.characteristicsAreFallback) {
659
+ // Fallback-schema is onbetrouwbaar → alleen markeren, niet droppen.
660
+ lastPrunedVbnRef.current = data.vbnCode;
661
+ return;
662
+ }
663
+ if (lastPrunedVbnRef.current === "") {
664
+ // Eerste run per component-instance = hydratie. Markeer en skip.
665
+ lastPrunedVbnRef.current = data.vbnCode;
666
+ return;
667
+ }
362
668
  lastPrunedVbnRef.current = data.vbnCode;
363
669
  const allowedByCode = new Map(spec.characteristics.map((c) => [c.code, c]));
364
670
  setData((d) => {
@@ -379,7 +685,38 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
379
685
  }
380
686
  return { ...d, features: next };
381
687
  });
382
- }, [data.vbnCode, spec.characteristics]);
688
+ }, [data.vbnCode, spec.characteristics, spec.characteristicsAreFallback]);
689
+ // Hydrateer vbnName wanneer we een code hebben maar (nog) geen naam — komt
690
+ // voor bij edit-flow: Floriday geeft alleen `vbnProductCode` terug, niet de
691
+ // omschrijving. Lookup gebeurt exact op record_id in de Floricode-cache.
692
+ const lastResolvedVbnNameRef = useRef("");
693
+ useEffect(() => {
694
+ const code = String(data.vbnCode ?? "").trim();
695
+ if (!code)
696
+ return;
697
+ if (data.vbnName && data.vbnName.length > 0)
698
+ return;
699
+ if (lastResolvedVbnNameRef.current === code)
700
+ return;
701
+ lastResolvedVbnNameRef.current = code;
702
+ let cancelled = false;
703
+ (async () => {
704
+ try {
705
+ const rows = await listFloricodeByField("VbnProduct", "record_id", code, 1);
706
+ const name = rows[0]?.data?.name ??
707
+ rows[0]?.data?.description ??
708
+ "";
709
+ if (!cancelled && name)
710
+ update("vbnName", name);
711
+ }
712
+ catch {
713
+ /* stille faal — trigger-label valt terug op de code */
714
+ }
715
+ })();
716
+ return () => {
717
+ cancelled = true;
718
+ };
719
+ }, [data.vbnCode, data.vbnName]);
383
720
  const validateEigenschappen = (d) => {
384
721
  const hasRegulatoryLookup = /^\d{5,6}$/.test(String(d.vbnCode ?? "").trim());
385
722
  if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
@@ -536,6 +873,35 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
536
873
  toast.error(`Prijs bijwerken mislukt: ${e.message}`);
537
874
  }
538
875
  }
876
+ // Basisprijs + prijzen per prijsgroep (weekly-base-supply +
877
+ // weekly-supply-lines) spiegelen naar trade_item_prices en pushen.
878
+ try {
879
+ const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
880
+ data.packingConfigurations[0];
881
+ const priceSync = await syncTradeItemPrices({
882
+ tradeItemId,
883
+ tradeItemKey: tradeItemId,
884
+ availableQuantity: data.saleStockPieces,
885
+ currency: "EUR",
886
+ periodStart: data.offerStartDate
887
+ ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
888
+ : undefined,
889
+ basePricePerPiece: data.salePricePerPiece,
890
+ pricesByGroupId: data.priceGroupPrices ?? {},
891
+ priceGroups,
892
+ packingConfigurationId: primary?.id,
893
+ });
894
+ const failed = priceSync.groupResults.filter((r) => !r.ok);
895
+ if (failed.length) {
896
+ toast.error(`Prijsgroep(en) niet gepusht: ${failed.map((r) => r.name).join(", ")}`);
897
+ }
898
+ else if (priceSync.groupResults.length) {
899
+ toast.success(`${priceSync.groupResults.length} prijsgroep-prijs(zen) bijgewerkt`);
900
+ }
901
+ }
902
+ catch (e) {
903
+ toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
904
+ }
539
905
  }
540
906
  else if (publishToFloriday) {
541
907
  const payload = buildAddTradeItemPayload(data);
@@ -563,23 +929,36 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
563
929
  // 1) Seed lokale voorraad (physical_stock) — de continuous-stock
564
930
  // trigger pusht dit naar Floriday zodra de trade item in de
565
931
  // cache verschijnt.
932
+ // GUARD: nooit een BESTAANDE voorraad-rij overschrijven. Als
933
+ // er al een rij is (bv. bij herpubliceren of bij een lokaal
934
+ // product met historische voorraad), laten we die met rust
935
+ // om ongewild op 0 zetten te voorkomen. Voorraad wordt
936
+ // beheerd via de Voorraadkaart, niet via de wizard.
566
937
  try {
567
938
  const key = stockKeyFor({ tradeItemId });
568
939
  if (key && data.saleStockPieces > 0) {
569
- const userRes = await supabase.auth.getUser();
570
- const userId = userRes.data.user?.id;
571
- if (userId) {
572
- await supabase
573
- .from("trade_item_stock")
574
- .upsert({
575
- user_id: userId,
576
- connection_id: activeId,
577
- trade_item_key: key,
578
- physical_stock: data.saleStockPieces,
579
- minimum_stock: 0,
580
- reserved_stock: 0,
581
- incoming_stock: 0,
582
- }, { onConflict: "connection_id,trade_item_key" });
940
+ const { data: existing } = await supabase
941
+ .from("trade_item_stock")
942
+ .select("trade_item_key")
943
+ .eq("connection_id", activeId)
944
+ .eq("trade_item_key", key)
945
+ .maybeSingle();
946
+ if (!existing) {
947
+ const userRes = await supabase.auth.getUser();
948
+ const userId = userRes.data.user?.id;
949
+ if (userId) {
950
+ await supabase
951
+ .from("trade_item_stock")
952
+ .insert({
953
+ user_id: userId,
954
+ connection_id: activeId,
955
+ trade_item_key: key,
956
+ physical_stock: data.saleStockPieces,
957
+ minimum_stock: 0,
958
+ reserved_stock: 0,
959
+ incoming_stock: 0,
960
+ });
961
+ }
583
962
  }
584
963
  }
585
964
  }
@@ -635,6 +1014,31 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
635
1014
  catch (e) {
636
1015
  toast.error(`Prijs online zetten mislukt: ${e.message}`);
637
1016
  }
1017
+ // Basisprijs + prijzen per prijsgroep spiegelen + pushen.
1018
+ try {
1019
+ const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
1020
+ data.packingConfigurations[0];
1021
+ const priceSync = await syncTradeItemPrices({
1022
+ tradeItemId,
1023
+ tradeItemKey: tradeItemId,
1024
+ availableQuantity: data.saleStockPieces,
1025
+ currency: "EUR",
1026
+ periodStart: data.offerStartDate
1027
+ ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
1028
+ : undefined,
1029
+ basePricePerPiece: data.salePricePerPiece,
1030
+ pricesByGroupId: data.priceGroupPrices ?? {},
1031
+ priceGroups,
1032
+ packingConfigurationId: primary?.id,
1033
+ });
1034
+ const failed = priceSync.groupResults.filter((r) => !r.ok);
1035
+ if (failed.length) {
1036
+ toast.error(`Prijsgroep(en) niet gepusht: ${failed.map((r) => r.name).join(", ")}`);
1037
+ }
1038
+ }
1039
+ catch (e) {
1040
+ toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
1041
+ }
638
1042
  }
639
1043
  else {
640
1044
  toast.success("Artikel aangemaakt in Floriday");
@@ -655,7 +1059,27 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
655
1059
  }
656
1060
  catch (e) {
657
1061
  const msg = e.message;
658
- if (msg.includes("NO_ACTIVE_FLORIDAY_CONNECTION")) {
1062
+ const missingCodes = isMissingCharacteristicsError(msg);
1063
+ if (missingCodes.length > 0) {
1064
+ // Floriday's server-side check gaf terug welke karakteristieken nog
1065
+ // ontbreken. Schrijf ze naar de gedeelde Supabase-cache zodat iedere
1066
+ // gebruiker deze eisen vanaf nu vóór de call te zien krijgt, en
1067
+ // navigeer terug naar Eigenschappen zodat ze meteen invulbaar zijn.
1068
+ if (data.vbnCode) {
1069
+ try {
1070
+ const res = await saveFloridayVbnRequired({
1071
+ data: { vbnCode: data.vbnCode, codes: missingCodes },
1072
+ });
1073
+ queryClient.setQueryData(["floriday-vbn-required", data.vbnCode], { vbnCode: res.vbnCode, requiredCodes: res.requiredCodes });
1074
+ }
1075
+ catch { /* cache-write mag stilletjes falen */ }
1076
+ }
1077
+ const eigenschappenIdx = visibleSteps(data).findIndex((s) => s.key === "eigenschappen");
1078
+ if (eigenschappenIdx >= 0)
1079
+ setStep(eigenschappenIdx);
1080
+ toast.error(`Floriday vereist nog: ${missingCodes.join(", ")}. Vul deze eigenschappen in en probeer opnieuw.`);
1081
+ }
1082
+ else if (msg.includes("NO_ACTIVE_FLORIDAY_CONNECTION")) {
659
1083
  toast.error("Geen actieve Floriday-connectie. Ga naar Instellingen → Floriday.");
660
1084
  }
661
1085
  else {
@@ -720,7 +1144,6 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
720
1144
  // een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
721
1145
  // meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
722
1146
  // het systeem beschikbaar zijn en niet verloren raken bij navigatie.
723
- const lastSavedRef = useRef(null);
724
1147
  useEffect(() => {
725
1148
  if (!activeId)
726
1149
  return;
@@ -730,7 +1153,10 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
730
1153
  if (!draftId && !pushTargetId)
731
1154
  return;
732
1155
  const serialized = JSON.stringify(data);
733
- if (lastSavedRef.current === null) {
1156
+ if (lastSavedRef.current === null || !hydrationSettledRef.current) {
1157
+ // Hydratie-effects (product/draft/cache load, prijzen, VBN-features prune)
1158
+ // mogen niet als user-edit gelden — schuif de baseline mee tot de wizard
1159
+ // echt tot rust is gekomen.
734
1160
  lastSavedRef.current = serialized;
735
1161
  return;
736
1162
  }
@@ -744,8 +1170,22 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
744
1170
  if (draftId) {
745
1171
  await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
746
1172
  }
1173
+ if (pushTargetId && !data.productGroup) {
1174
+ setSyncState(draftId ? "local" : "idle");
1175
+ lastSavedRef.current = serialized;
1176
+ return;
1177
+ }
747
1178
  // Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
748
1179
  if (pushTargetId) {
1180
+ // Geen warehouse gekozen? Sla stil lokaal op — geen toast, geen rode
1181
+ // pill. Zodra de gebruiker een warehouse kiest gaat de sync verder.
1182
+ const whId = (data.warehouseId ?? "").trim();
1183
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(whId);
1184
+ if (!isUuid) {
1185
+ setSyncState(draftId ? "local" : "idle");
1186
+ lastSavedRef.current = serialized;
1187
+ return;
1188
+ }
749
1189
  await saveWarehousePreference(pushTargetId, data.warehouseId);
750
1190
  const payload = buildUpdateTradeItemPayload(data);
751
1191
  const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
@@ -808,7 +1248,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
808
1248
  if (!activeConnection) {
809
1249
  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." }) }));
810
1250
  }
811
- 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 || (!draftId && !vbnValid), children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), "Product opslaan"] })), (isEditingDraft || isEditingProduct) && (_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: "Product verwijderen?" }), _jsx(AlertDialogDescription, { children: "Dit product wordt permanent uit de voorraad verwijderd. Als het product actief op Floriday staat moet je het daar apart deactiveren." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteDraft, children: "Verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), 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, 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 && (_jsxs(_Fragment, { children: [_jsx(ArtikelCompositionCard, { productId: linkedProductId, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: linkedProductId }), _jsx(OrderHistoryCard, { tradeItemKey: localTradeItemKey, productName: data.name || data.vbnName || null, productId: linkedProductId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: localTradeItemKey, productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
1251
+ return (_jsxs("main", { className: "p-6 space-y-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 || (!draftId && !vbnValid), children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), "Product opslaan"] })), (isEditingDraft || isEditingProduct) && (_jsxs(AlertDialog, { open: deleteOpen, onOpenChange: (o) => {
1252
+ setDeleteOpen(o);
1253
+ if (!o)
1254
+ setDeleteAck(false);
1255
+ }, 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: "Product verwijderen?" }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { children: ["Je staat op het punt om ", _jsx("strong", { children: data.name || "dit artikel" }), data.articleCode ? _jsxs(_Fragment, { children: [" (artikelnummer ", _jsx("code", { children: data.articleCode }), ")"] }) : null, " ", "lokaal uit de voorraad te verwijderen."] }), _jsxs("div", { className: "rounded-md border p-3 bg-muted/40 space-y-1", children: [_jsxs("div", { children: ["Huidige voorraad: ", _jsx("strong", { children: productMeta.physicalStock }), " stuks"] }), _jsxs("div", { children: ["Floriday-koppeling:", " ", productMeta.floridayActive || productMeta.floridayTradeItemId ? (_jsx("span", { className: "text-emerald-700 font-medium", children: "actief" })) : (_jsx("span", { className: "text-muted-foreground", children: "geen" }))] })] }), (productMeta.floridayActive || productMeta.floridayTradeItemId) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900", children: [_jsx("strong", { children: "Let op:" }), " dit artikel is gekoppeld met Floriday. Als je alleen de lokale kant verwijdert, blijft het artikel op Floriday bestaan en verschijnt het bij de volgende sync weer terug in de lijst. Trek het artikel daarna zelf in via het Floriday-portaal als je het echt weg wilt."] })), productMeta.physicalStock > 0 && (_jsxs("div", { className: "rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-900", children: ["Er staat nog voorraad (", productMeta.physicalStock, " stuks) op dit product. Deze voorraad wordt \u00F3\u00F3k verwijderd en kan niet automatisch worden hersteld."] })), _jsxs("label", { className: "flex items-start gap-2 pt-1", children: [_jsx(Checkbox, { checked: deleteAck, onCheckedChange: (v) => setDeleteAck(Boolean(v)) }), _jsx("span", { children: "Ik weet zeker dat ik dit product wil verwijderen en begrijp de gevolgen." })] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { disabled: !deleteAck, onClick: handleDeleteDraft, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Definitief verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), 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, 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 && (_jsxs(_Fragment, { children: [_jsx(ArtikelCompositionCard, { productId: linkedProductId, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1256
+ name: data.name || data.vbnName,
1257
+ articleNumber: data.articleCode,
1258
+ gtin: data.gtin,
1259
+ } }), _jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: linkedProductId }), _jsx(OrderHistoryCard, { tradeItemKey: localTradeItemKey, productName: data.name || data.vbnName || null, productId: linkedProductId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: localTradeItemKey, productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
812
1260
  const isOpen = i === step;
813
1261
  const isValid = stepValidity[i];
814
1262
  const isVisited = i < step || isEditing;
@@ -830,7 +1278,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
830
1278
  ? "border-primary bg-primary text-primary-foreground"
831
1279
  : status === "invalid"
832
1280
  ? "border-destructive bg-destructive text-destructive-foreground"
833
- : "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, 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
1281
+ : "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, priceGroups: priceGroups, parentProductId: linkedProductId ?? draftId ?? null, parentFloridayId: draftFloridayId ?? tradeItemId ?? null, articleCodeLocked: Boolean(draftFloridayId || tradeItemId) }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo, 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
834
1282
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
835
1283
  : "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
836
1284
  ? "Wijzigingen opslaan"
@@ -885,9 +1333,13 @@ function summaryFor(key, d) {
885
1333
  ? `${d.additionalPackaging.length} item(s)`
886
1334
  : "Geen extra verpakking";
887
1335
  case "samenstelling":
888
- return "Binnenkort beschikbaar";
1336
+ return "Basis + per belading";
889
1337
  case "beschikbaarheid":
890
- return d.yearRound ? "Jaarrond" : `${d.weeks.length} week/weken`;
1338
+ if (d.availabilityMode === "YEAR_ROUND")
1339
+ return "Jaarrond";
1340
+ if (d.availabilityMode === "UNSPECIFIED")
1341
+ return "Niet gespecificeerd";
1342
+ return `${d.weeks.length} week/weken`;
891
1343
  case "aanbieden":
892
1344
  if (!d.offerForSale)
893
1345
  return "Niet aanbieden";
@@ -920,7 +1372,10 @@ function StepBody(p) {
920
1372
  case "verpakking":
921
1373
  return _jsx(StepVerpakking, { ...p });
922
1374
  case "samenstelling":
923
- return (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground", children: "Binnenkort beschikbaar" }));
1375
+ return (_jsx(StepSamenstelling, { productId: p.parentProductId, floridayId: p.parentFloridayId, packings: p.data.packingConfigurations, packagingTypeLabel: (code) => {
1376
+ const rec = p.packagingTypes.find((r) => codeOf(r) === code);
1377
+ return rec ? labelOf(rec) : code;
1378
+ } }));
924
1379
  case "beschikbaarheid":
925
1380
  return _jsx(StepBeschikbaarheid, { ...p });
926
1381
  case "aanbieden":
@@ -955,11 +1410,11 @@ function groupOfRecord(r) {
955
1410
  const n = Number(raw);
956
1411
  if (Number.isFinite(n) && n > 0) {
957
1412
  const s = String(Math.floor(n));
958
- if (s.startsWith("10"))
1413
+ if (s.startsWith("10") || s.startsWith("11"))
959
1414
  return "CUT_FLOWER";
960
- if (s.startsWith("20"))
1415
+ if (s.startsWith("20") || s.startsWith("21") || s.startsWith("29"))
961
1416
  return "POT_PLANT";
962
- if (s.startsWith("30"))
1417
+ if (s.startsWith("30") || s.startsWith("31"))
963
1418
  return "GARDEN_PLANT";
964
1419
  return null;
965
1420
  }
@@ -1056,8 +1511,12 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
1056
1511
  setQuery("");
1057
1512
  };
1058
1513
  const triggerLabel = data.vbnName
1059
- ? data.vbnName
1060
- : "Selecteer een VBN productcode";
1514
+ ? data.vbnCode
1515
+ ? `${data.vbnCode} – ${data.vbnName}`
1516
+ : data.vbnName
1517
+ : data.vbnCode
1518
+ ? data.vbnCode
1519
+ : "Selecteer een VBN productcode";
1061
1520
  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) => {
1062
1521
  const next = v === "ALL" ? null : v;
1063
1522
  update("productGroup", next);
@@ -1092,7 +1551,7 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
1092
1551
  function InfoTip({ text }) {
1093
1552
  return (_jsx("span", { title: text, className: "inline-flex cursor-help text-muted-foreground", children: _jsx(Info, { className: "h-3.5 w-3.5" }) }));
1094
1553
  }
1095
- function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
1554
+ function StepArtikel({ data, update, collectionsList, setCollectionsList, articleCodeLocked }) {
1096
1555
  const [newCollection, setNewCollection] = useState("");
1097
1556
  const [showNewCollection, setShowNewCollection] = useState(false);
1098
1557
  const addCollection = async (name) => {
@@ -1122,7 +1581,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
1122
1581
  icon: ClipboardList,
1123
1582
  },
1124
1583
  ];
1125
- 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) => {
1584
+ 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 ", !articleCodeLocked && _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), disabled: articleCodeLocked, readOnly: articleCodeLocked }), articleCodeLocked && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Artikelcode ligt vast in Floriday en kan niet meer gewijzigd worden." }))] }), _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) => {
1126
1585
  if (v === "__new__") {
1127
1586
  setShowNewCollection(true);
1128
1587
  return;
@@ -1144,7 +1603,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
1144
1603
  } }), _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: () => {
1145
1604
  setShowNewCollection(false);
1146
1605
  setNewCollection("");
1147
- }, 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) => {
1606
+ }, children: "Annuleer" })] })), _jsx("p", { className: "text-xs text-muted-foreground", children: "Collecties worden voorlopig lokaal opgeslagen; koppeling met Floriday-collecties volgt in een aparte update." })] }), _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) => {
1148
1607
  const Icon = t.icon;
1149
1608
  const active = data.articleType === t.value;
1150
1609
  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
@@ -1207,7 +1666,10 @@ function StepPaspoort({ data, update }) {
1207
1666
  : "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) => {
1208
1667
  const checked = selectedCountries.includes(c.code);
1209
1668
  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));
1210
- })] })] })] })] })] }));
1669
+ })] })] })] }), selectedCountries.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: selectedCountries.map((code) => {
1670
+ const name = COUNTRIES.find((o) => o.code === code)?.name ?? code;
1671
+ return (_jsxs("span", { className: "inline-flex items-center gap-2 rounded-md border border-input bg-background px-2.5 py-1 text-sm", children: [name, _jsx("button", { type: "button", "aria-label": `Verwijder ${name}`, onClick: () => toggleCountry(code), 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" }) })] }, code));
1672
+ }) }))] })] }));
1211
1673
  }
1212
1674
  function StepEigenschappen({ data, update, spec, schemaQuery, regulatoryQuery }) {
1213
1675
  if (!data.vbnCode) {
@@ -1265,6 +1727,11 @@ function EigenschappenBody({ data, update, spec, }) {
1265
1727
  const hasValue = (code) => Boolean((data.features[code] ?? "").trim());
1266
1728
  const visibleFields = spec.characteristics.filter((c) => c.required || visibleOptional.has(c.code) || hasValue(c.code));
1267
1729
  const hiddenOptional = spec.characteristics.filter((c) => !c.required && !visibleOptional.has(c.code) && !hasValue(c.code));
1730
+ // Codes die Floriday teruggeeft maar die het lokale spec-schema niet kent.
1731
+ // Deze mogen NIET stil worden gedropt — we tonen ze als bewerkbaar veld zodat
1732
+ // de gebruiker ze kan wijzigen of verwijderen, en de PUT-payload behoudt ze.
1733
+ const knownCodes = new Set(spec.characteristics.map((c) => c.code));
1734
+ const extraCodes = Object.keys(data.features).filter((code) => hasValue(code) && !knownCodes.has(code));
1268
1735
  const filteredHidden = hiddenOptional.filter((c) => {
1269
1736
  if (!addSearch.trim())
1270
1737
  return true;
@@ -1291,7 +1758,13 @@ function EigenschappenBody({ data, update, spec, }) {
1291
1758
  delete nextFeatures[code];
1292
1759
  update("features", nextFeatures);
1293
1760
  };
1294
- 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)
1761
+ 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))), extraCodes.map((code) => (_jsx(CharacteristicField, { field: {
1762
+ code,
1763
+ label: code,
1764
+ required: false,
1765
+ listName: code,
1766
+ allowedValues: [],
1767
+ }, value: data.features[code] || "", onChange: (v) => update("features", { ...data.features, [code]: v }), onRemove: () => removeField(code) }, code))), _jsx("div", { children: _jsxs(Popover, { open: addOpen, onOpenChange: (o) => { setAddOpen(o); if (!o)
1295
1768
  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
1296
1769
  ? "Alle beschikbare karakteristieken zijn al toegevoegd."
1297
1770
  : "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)))) })] })] }) })] }));
@@ -1472,7 +1945,34 @@ function StepFotos({ data, update, activeId }) {
1472
1945
  }, 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) => {
1473
1946
  onFiles(e.target.files);
1474
1947
  e.target.value = "";
1475
- } })] }), 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))) }))] }));
1948
+ } })] }), data.images.length > 0 && (_jsx("div", { className: "grid grid-cols-4 gap-3", children: data.images.map((img, idx) => {
1949
+ const setImages = (fn) => update("images", fn(data.images));
1950
+ const removeAt = () => setImages((arr) => arr.filter((_, i) => i !== idx));
1951
+ const setPrimary = () => setImages((arr) => arr.map((it, k) => ({ ...it, primary: k === idx })));
1952
+ const move = (dir) => setImages((arr) => {
1953
+ const target = idx + dir;
1954
+ if (target < 0 || target >= arr.length)
1955
+ return arr;
1956
+ const next = arr.slice();
1957
+ [next[idx], next[target]] = [next[target], next[idx]];
1958
+ return next;
1959
+ });
1960
+ const setType = (v) => setImages((arr) => arr.map((it, k) => (k === idx ? { ...it, type: v } : it)));
1961
+ return (_jsxs("div", { className: "relative group rounded-md overflow-hidden border bg-background", children: [_jsxs("div", { className: "relative", children: [_jsx("img", { src: img.previewUrl || img.url, alt: img.name, className: "h-28 w-full object-cover" }), img.primary && (_jsxs("span", { className: "absolute top-1 left-1 inline-flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground", children: [_jsx(Star, { className: "h-3 w-3 fill-current" }), " Hoofdfoto"] })), _jsx("button", { type: "button", onClick: removeAt, "aria-label": "Verwijder foto", className: "absolute top-1 right-1 rounded-full bg-background/80 p-1 opacity-0 transition group-hover:opacity-100", children: _jsx(X, { className: "h-3 w-3" }) })] }), _jsxs("div", { className: "grid gap-1.5 p-2", children: [_jsxs(Select, { value: img.type ?? "WEBSHOPFRIENDLY", onValueChange: setType, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: [
1962
+ ["WEBSHOPFRIENDLY", "Standaard (webshop)"],
1963
+ ["PIECE", "Stuk"],
1964
+ ["TRAY", "Tray"],
1965
+ ["DETAIL", "Detail"],
1966
+ ["RULER", "Meetlat"],
1967
+ ["SCENE", "Scène"],
1968
+ ["SEASON", "Seizoen"],
1969
+ ["STEM", "Steel"],
1970
+ ["BUNCH", "Bos"],
1971
+ ["CONTAINER", "Container"],
1972
+ ["CLOCK", "Klok"],
1973
+ ["UNKNOWN", "Onbekend"],
1974
+ ].map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] }), _jsxs("div", { className: "flex items-center justify-between gap-1", children: [_jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => move(-1), disabled: idx === 0, "aria-label": "Verplaats naar links", children: _jsx(ArrowLeft, { className: "h-3.5 w-3.5" }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => move(1), disabled: idx === data.images.length - 1, "aria-label": "Verplaats naar rechts", children: _jsx(ArrowRight, { className: "h-3.5 w-3.5" }) })] }), !img.primary && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 px-2 text-[11px]", onClick: setPrimary, children: [_jsx(Star, { className: "mr-1 h-3 w-3" }), " Hoofdfoto"] }))] })] })] }, img.url));
1975
+ }) }))] }));
1476
1976
  }
1477
1977
  /**
1478
1978
  * Warehouse-stap — verplicht in Floriday Suppliers API 2026v1.
@@ -1571,12 +2071,12 @@ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCu
1571
2071
  const editing = editingId
1572
2072
  ? data.packingConfigurations.find((p) => p.id === editingId) ?? null
1573
2073
  : null;
1574
- 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) => {
2074
+ 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: "Belading" }), _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) => {
1575
2075
  const pkgLabel = packagingTypes.find((r) => codeOf(r) === p.packagingType);
1576
2076
  const lcLabel = loadCarriers.find((r) => codeOf(r) === p.loadCarrier);
1577
2077
  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
1578
2078
  ? `${codeOf(pkgLabel)}: ${labelOf(pkgLabel)}`
1579
- : 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: LOAD_CARRIER_LABELS[p.loadCarrier] ?? 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));
2079
+ : 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: LOAD_CARRIER_LABELS[p.loadCarrier] ?? p.loadCarrier ?? "—" }), _jsxs("td", { className: "px-3 py-2 tabular-nums", children: [p.layersPerLoadCarrier, "\u00D7", p.packagesPerLayer, "\u00D7", p.unitsPerPackage] }), _jsx("td", { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center gap-1", 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" })), p.isHiddenForDirectSales && (_jsx("span", { className: "rounded bg-slate-200 px-2 py-0.5 text-xs text-slate-700", children: "Verborgen" }))] }) }), _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));
1580
2080
  }) })] }) })), _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) => {
1581
2081
  setDialogOpen(o);
1582
2082
  if (!o)
@@ -1598,6 +2098,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1598
2098
  isCustomerSpecific: false,
1599
2099
  customerOrganizationIds: [],
1600
2100
  isPrimary: isFirst,
2101
+ isHiddenForDirectSales: false,
1601
2102
  });
1602
2103
  const [draft, setDraft] = useState(emptyItem);
1603
2104
  const [pkgSearch, setPkgSearch] = useState("");
@@ -1627,8 +2128,16 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1627
2128
  ? draft.customerOrganizationIds.filter((x) => x !== id)
1628
2129
  : [...draft.customerOrganizationIds, id]);
1629
2130
  };
2131
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2132
+ const materialIsUuid = UUID_RE.test((draft.packagingMaterialId ?? "").trim());
2133
+ const supplierIsUuid = UUID_RE.test((draft.packagingSupplierId ?? "").trim());
2134
+ const hasCustomPkg = draft.source === "SUPPLIER" && materialIsUuid;
2135
+ const materialInvalid = draft.source === "SUPPLIER" &&
2136
+ !!(draft.packagingMaterialId ?? "").trim() &&
2137
+ !materialIsUuid;
1630
2138
  const canSave = draft.loadCarrier.length > 0 &&
1631
- draft.packagingType.length > 0 &&
2139
+ (hasCustomPkg || draft.packagingType.length > 0) &&
2140
+ !materialInvalid &&
1632
2141
  (draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
1633
2142
  (!draft.isCustomerSpecific || draft.customerOrganizationIds.length > 0);
1634
2143
  const onUploadPhoto = async (file) => {
@@ -1665,14 +2174,22 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1665
2174
  ? "border-primary bg-primary/5"
1666
2175
  : "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"
1667
2176
  ? "border-primary bg-primary/5"
1668
- : "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) => {
1669
- const sup = PACKAGING_SUPPLIERS.find((s) => s.id === v);
1670
- setDraft((d) => ({
1671
- ...d,
1672
- packagingSupplierId: v,
1673
- packagingSupplierName: sup?.name,
1674
- }));
1675
- }, 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
2177
+ : "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: "space-y-3", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Verpakkingsleverancier" }), supplierIsUuid ? (_jsxs("div", { className: "rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsx("div", { className: "font-medium", children: draft.packagingSupplierName ?? "Gekoppelde leverancier" }), _jsxs("div", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-mono truncate", children: draft.packagingSupplierId }), _jsx("button", { type: "button", className: "text-primary underline", onClick: () => setDraft((d) => ({
2178
+ ...d,
2179
+ packagingSupplierId: undefined,
2180
+ packagingSupplierName: undefined,
2181
+ })), children: "Wijzig" })] })] })) : (_jsxs(Select, { value: draft.packagingSupplierId || undefined, onValueChange: (v) => {
2182
+ const sup = PACKAGING_SUPPLIERS.find((s) => s.id === v);
2183
+ setDraft((d) => ({
2184
+ ...d,
2185
+ packagingSupplierId: v,
2186
+ packagingSupplierName: sup?.name,
2187
+ }));
2188
+ }, 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: "Floricode VRS-materiaal UUID (optioneel)" }), _jsx(Input, { value: draft.packagingMaterialId ?? "", onChange: (e) => patch("packagingMaterialId", e.target.value.trim()), placeholder: "00000000-0000-0000-0000-000000000000", className: materialInvalid ? "border-destructive" : "" }), materialInvalid ? (_jsx("p", { className: "text-xs text-destructive", children: "Ongeldig UUID-formaat. Laat leeg of plak een echte VRS-materiaal UUID uit Floricode." })) : (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wanneer ingevuld gaat het als ", _jsx("code", { children: "customPackageId" }), "naar Floriday en vervangt het de VBN-fust."] }))] })] }), hasCustomPkg && (_jsx("div", { className: "rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs", children: "Custom package actief \u2014 VBN-fust hieronder wordt genegeerd in de Floriday-payload." }))] })), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: hasCustomPkg
2189
+ ? "VBN-fust (genegeerd — custom package actief)"
2190
+ : draft.source === "SUPPLIER"
2191
+ ? "Fallback VBN-fust"
2192
+ : "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
1676
2193
  ? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
1677
2194
  ? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
1678
2195
  : draft.packagingType)
@@ -1684,7 +2201,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1684
2201
  photoUrl: undefined,
1685
2202
  photoImageId: undefined,
1686
2203
  photoPreviewUrl: undefined,
1687
- })), 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" })] })] }) }));
2204
+ })), 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("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: Boolean(draft.isHiddenForDirectSales), onChange: (e) => patch("isHiddenForDirectSales", e.target.checked) }), "Verbergen voor Direct Sale (isHiddenForDirectSales)"] })] }), _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" })] })] }) }));
1688
2205
  }
1689
2206
  function StepVerpakking({ data, update }) {
1690
2207
  const [dialogOpen, setDialogOpen] = useState(false);
@@ -1693,14 +2210,14 @@ function StepVerpakking({ data, update }) {
1693
2210
  return (_jsxs("div", { className: "space-y-3", children: [_jsxs("p", { className: "text-xs text-muted-foreground bg-muted/40 rounded-md p-2 border", children: ["Voeg extra verpakkingsmateriaal (hoes, steker, label, potfolie, \u2026) uit de VRS-catalogus toe. Materialen met een geldige Floriday-UUID worden automatisch meegestuurd naar Floriday als", _jsx("code", { className: "mx-1", children: "additionalPackagingMaterialIds" }), "op het TradeItem."] }), data.additionalPackaging.length > 0 && (_jsx("div", { className: "border rounded-md divide-y", children: data.additionalPackaging.map((p) => (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] gap-3 items-center px-3 py-2 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Leverancier" }), _jsx("div", { children: p.packagingSupplierName })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Type" }), _jsx("div", { children: p.materialTypeName })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Materiaal" }), _jsx("div", { children: p.packagingMaterialName })] }), _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: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Verpakkingsmateriaal toevoegen"] }), _jsx(AdditionalPackagingDialog, { open: dialogOpen, onOpenChange: setDialogOpen, onSave: addItem })] }));
1694
2211
  }
1695
2212
  function StepBeschikbaarheid({ data, update }) {
1696
- const weeks = Array.from({ length: 52 }, (_, i) => i + 1);
2213
+ const weeks = Array.from({ length: 53 }, (_, i) => i + 1);
1697
2214
  // Drag-selectie: bij mousedown bepalen we of we in "select"- of "deselect"-
1698
2215
  // modus zitten (op basis van de startcel) en breiden we de selectie uit
1699
2216
  // terwijl de gebruiker over de weken sleept, zoals in Floriday.
1700
2217
  const dragStateRef = useRef(null);
1701
2218
  const [dragHover, setDragHover] = useState(null);
1702
2219
  const setSelection = (list) => {
1703
- const unique = Array.from(new Set(list.filter((w) => w >= 1 && w <= 52)))
2220
+ const unique = Array.from(new Set(list.filter((w) => w >= 1 && w <= 53)))
1704
2221
  .sort((a, b) => a - b);
1705
2222
  update("weeks", unique);
1706
2223
  };
@@ -1806,7 +2323,14 @@ function StepBeschikbaarheid({ data, update }) {
1806
2323
  pointerXRef.current = e.clientX;
1807
2324
  startAutoScrollLoop();
1808
2325
  };
1809
- 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: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsx(Label, { className: "shrink-0", children: "Selecteer weken" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Sleep om meerdere weken te selecteren", data.weeks.length > 0 && ` — ${data.weeks.length} week/weken`] }), _jsx("button", { type: "button", onClick: () => setSelection([]), disabled: data.weeks.length === 0, className: "rounded-md border px-2.5 py-1 text-xs text-destructive hover:border-destructive/40 hover:bg-destructive/5 disabled:cursor-not-allowed disabled:opacity-40", children: "Selectie leegmaken" })] })] }), _jsx("div", { ref: scrollRef, className: "overflow-x-auto pb-2", children: _jsx("div", { className: "flex select-none gap-1 min-w-max", children: weeks.map((w) => {
2326
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex flex-wrap gap-2", children: [
2327
+ { value: "YEAR_ROUND", label: "Jaarrond" },
2328
+ { value: "SPECIFIC", label: "Specifieke periode" },
2329
+ { value: "UNSPECIFIED", label: "Niet gespecificeerd" },
2330
+ ].map((opt) => {
2331
+ const active = data.availabilityMode === opt.value;
2332
+ return (_jsx("button", { type: "button", onClick: () => update("availabilityMode", opt.value), className: `rounded-md border px-3 py-1.5 text-sm ${active ? "border-primary bg-primary/5" : "hover:border-primary/40"}`, children: opt.label }, opt.value));
2333
+ }) }), data.availabilityMode === "SPECIFIC" && (_jsxs("div", { children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsx(Label, { className: "shrink-0", children: "Selecteer weken" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Sleep om meerdere weken te selecteren", data.weeks.length > 0 && ` — ${data.weeks.length} week/weken`] }), _jsx("button", { type: "button", onClick: () => setSelection([]), disabled: data.weeks.length === 0, className: "rounded-md border px-2.5 py-1 text-xs text-destructive hover:border-destructive/40 hover:bg-destructive/5 disabled:cursor-not-allowed disabled:opacity-40", children: "Selectie leegmaken" })] })] }), _jsx("div", { ref: scrollRef, className: "overflow-x-auto pb-2", children: _jsx("div", { className: "flex select-none gap-1 min-w-max", children: weeks.map((w) => {
1810
2334
  const selected = data.weeks.includes(w);
1811
2335
  const isHover = dragHover === w;
1812
2336
  return (_jsx("button", { type: "button", "data-week": w, onMouseDown: (e) => onWeekMouseDownWithScroll(w, e), onMouseEnter: () => onWeekMouseEnter(w), className: `h-9 w-11 shrink-0 rounded border text-xs transition-colors ${selected
@@ -1817,18 +2341,56 @@ function StepBeschikbaarheid({ data, update }) {
1817
2341
  // ------------------------------------------------------------
1818
2342
  // Stap "Aanbieden & voorraad" — Direct Sale via CustomerOffer
1819
2343
  // ------------------------------------------------------------
1820
- function StepAanbieden({ data, update }) {
2344
+ function StepAanbieden({ data, update, priceGroups }) {
1821
2345
  const priceStr = Number.isFinite(data.salePricePerPiece)
1822
2346
  ? String(data.salePricePerPiece)
1823
2347
  : "";
1824
2348
  const stockStr = Number.isFinite(data.saleStockPieces)
1825
2349
  ? String(data.saleStockPieces)
1826
2350
  : "";
1827
- return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm text-muted-foreground", children: ["Zet dit product direct in de verkoop bij je gekoppelde Floriday-klanten. We sturen een ", _jsx("code", { children: "TradeItem" }), " + ", _jsx("code", { children: "CustomerOffer" }), " met vaste prijs per stuk en publiceren de opgegeven voorraad via de continuous-stock feed."] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Direct aanbieden voor verkoop" }), _jsx("div", { className: "text-xs text-muted-foreground", children: data.offerForSale
2351
+ const setGroupPrice = (groupId, raw) => {
2352
+ const next = { ...(data.priceGroupPrices ?? {}) };
2353
+ const parsed = Number(raw);
2354
+ if (!raw || !Number.isFinite(parsed) || parsed <= 0) {
2355
+ delete next[groupId];
2356
+ }
2357
+ else {
2358
+ next[groupId] = parsed;
2359
+ }
2360
+ update("priceGroupPrices", next);
2361
+ };
2362
+ return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm text-muted-foreground", children: ["Zet dit product direct in de verkoop bij je gekoppelde Floriday-klanten. We sturen een ", _jsx("code", { children: "TradeItem" }), " + ", _jsx("code", { children: "CustomerOffer" }), " met vaste basisprijs per stuk en publiceren de opgegeven voorraad via de continuous-stock feed. Prijzen per prijsgroep worden als aparte", " ", _jsx("code", { children: "weekly-supply-lines" }), " naar Floriday gepusht."] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Direct aanbieden voor verkoop" }), _jsx("div", { className: "text-xs text-muted-foreground", children: data.offerForSale
1828
2363
  ? "Aanbod wordt aangemaakt direct na publiceren."
1829
- : "Alleen artikel publiceren, geen aanbod aanmaken." })] }), _jsx(Switch, { checked: data.offerForSale, onCheckedChange: (v) => update("offerForSale", Boolean(v)) })] }), data.offerForSale && (_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Voorraad (stuks)" }), _jsx(Input, { type: "number", inputMode: "numeric", min: 0, step: 1, value: stockStr, onChange: (e) => update("saleStockPieces", Math.max(0, Math.floor(Number(e.target.value) || 0))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt als fysieke voorraad geboekt en via continuous-stock naar Floriday gepusht." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Verkoopprijs per stuk (EUR)" }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", value: priceStr, onChange: (e) => update("salePricePerPiece", Number(e.target.value) || 0) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Vaste prijs per stuk voor Direct Sale." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aanbod geldig vanaf" }), _jsx(Input, { type: "date", value: data.offerStartDate, onChange: (e) => update("offerStartDate", e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Aanbod loopt automatisch tot 1 jaar na deze datum." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Zichtbaar voor" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: data.customerOrganizationIds.length
1830
- ? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
1831
- : "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }))] }));
2364
+ : "Alleen artikel publiceren, geen aanbod aanmaken." })] }), _jsx(Switch, { checked: data.offerForSale, onCheckedChange: (v) => update("offerForSale", Boolean(v)) })] }), data.offerForSale && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Voorraad (stuks)" }), _jsx(Input, { type: "number", inputMode: "numeric", min: 0, step: 1, value: stockStr, onChange: (e) => update("saleStockPieces", Math.max(0, Math.floor(Number(e.target.value) || 0))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt als fysieke voorraad geboekt en via continuous-stock naar Floriday gepusht." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Basisprijs per stuk (EUR)" }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", value: priceStr, onChange: (e) => update("salePricePerPiece", Number(e.target.value) || 0) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wordt gepusht als ", _jsx("code", { children: "weekly-base-supply" }), " \u2014 de standaard catalogusprijs voor klanten zonder eigen prijsgroep."] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aanbod geldig vanaf" }), _jsx(Input, { type: "date", value: data.offerStartDate, onChange: (e) => update("offerStartDate", e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Aanbod loopt automatisch tot 1 jaar na deze datum." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Zichtbaar voor" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: data.customerOrganizationIds.length
2365
+ ? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
2366
+ : "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }), _jsxs("div", { className: "space-y-2 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Prijzen per Floriday-prijsgroep" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Laat leeg om voor die groep g\u00E9\u00E9n afwijkende prijs te publiceren. Basisprijs blijft dan gelden." })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [priceGroups.length, " groep", priceGroups.length === 1 ? "" : "en"] })] }), priceGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-4", children: "Geen actieve prijsgroepen gevonden voor deze Floriday-connectie." })) : (_jsx("div", { className: "grid gap-3 sm:grid-cols-2 pt-2", children: priceGroups.map((g) => {
2367
+ const val = data.priceGroupPrices?.[g.id];
2368
+ return (_jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "text-sm", children: [g.name, _jsxs("span", { className: "ml-2 text-xs font-normal text-muted-foreground", children: [g.customerOrganizationIds.length, " klant", g.customerOrganizationIds.length === 1 ? "" : "en"] })] }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", placeholder: "\u20AC", value: val ? String(val) : "", onChange: (e) => setGroupPrice(g.id, e.target.value) })] }, g.id));
2369
+ }) }))] })] })), _jsx(VatSection, { data: data, update: update })] }));
2370
+ }
2371
+ // ------------------------------------------------------------
2372
+ // BTW-blok (facturatie) — lokaal, niet naar Floriday
2373
+ // ------------------------------------------------------------
2374
+ const TWINFIELD_VAT_OPTIONS = [
2375
+ { code: "VH", label: "VH – Hoog (21%)", rate: 21 },
2376
+ { code: "VL", label: "VL – Laag (9%)", rate: 9 },
2377
+ { code: "VN", label: "VN – Nul (0%)", rate: 0 },
2378
+ { code: "VG", label: "VG – Geen / vrijgesteld", rate: 0 },
2379
+ ];
2380
+ function VatSection({ data, update }) {
2381
+ const code = data.vatCode ?? "";
2382
+ const onCodeChange = (next) => {
2383
+ if (next === "__default__") {
2384
+ update("vatCode", null);
2385
+ update("vatRate", null);
2386
+ return;
2387
+ }
2388
+ update("vatCode", next);
2389
+ const match = TWINFIELD_VAT_OPTIONS.find((o) => o.code === next);
2390
+ if (match)
2391
+ update("vatRate", match.rate);
2392
+ };
2393
+ return (_jsxs("div", { className: "space-y-3 rounded-md border p-4", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "BTW (facturatie)" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wordt gebruikt bij het maken van facturen. Leeg = standaardwaarde uit de instellingen (default 9% / VL)." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "BTW-tarief (%)" }), _jsxs(Select, { value: code || "__default__", onValueChange: onCodeChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__default__", children: "Standaard (VL \u2013 9%)" }), TWINFIELD_VAT_OPTIONS.map((o) => (_jsx(SelectItem, { value: o.code, children: o.label }, o.code)))] })] })] })] }));
1832
2394
  }
1833
2395
  // ============================================================
1834
2396
  // Shared