@flowselections/floriday-voorraad 1.0.23 → 1.0.27

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 (106) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +4 -4
  2. package/dist-lib/components/TemplatePage.d.ts +1 -1
  3. package/dist-lib/components/TemplatePage.d.ts.map +1 -1
  4. package/dist-lib/components/voorraad/AdditionalPackagingDialog.d.ts +1 -1
  5. package/dist-lib/components/voorraad/AdditionalPackagingDialog.d.ts.map +1 -1
  6. package/dist-lib/components/voorraad/ArtikelCompositionCard.d.ts +1 -1
  7. package/dist-lib/components/voorraad/ArtikelCompositionCard.d.ts.map +1 -1
  8. package/dist-lib/components/voorraad/ArtikelWizard.d.ts +1 -1
  9. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  10. package/dist-lib/components/voorraad/ArtikelWizard.js +633 -101
  11. package/dist-lib/components/voorraad/ConnectionBadge.d.ts +1 -1
  12. package/dist-lib/components/voorraad/ConnectionBadge.d.ts.map +1 -1
  13. package/dist-lib/components/voorraad/EditArtikelPage.d.ts +1 -1
  14. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
  15. package/dist-lib/components/voorraad/ExcelExportButton.d.ts +1 -1
  16. package/dist-lib/components/voorraad/ExcelExportButton.d.ts.map +1 -1
  17. package/dist-lib/components/voorraad/FloridayInfoCard.d.ts +1 -1
  18. package/dist-lib/components/voorraad/FloridayInfoCard.d.ts.map +1 -1
  19. package/dist-lib/components/voorraad/FloridayStatusBadge.d.ts +1 -1
  20. package/dist-lib/components/voorraad/FloridayStatusBadge.d.ts.map +1 -1
  21. package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts +1 -1
  22. package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts.map +1 -1
  23. package/dist-lib/components/voorraad/OrderHistoryCard.d.ts +1 -1
  24. package/dist-lib/components/voorraad/OrderHistoryCard.d.ts.map +1 -1
  25. package/dist-lib/components/voorraad/PriceStatusBadge.d.ts +1 -1
  26. package/dist-lib/components/voorraad/PriceStatusBadge.d.ts.map +1 -1
  27. package/dist-lib/components/voorraad/ProductLinkCard.d.ts +1 -1
  28. package/dist-lib/components/voorraad/ProductLinkCard.d.ts.map +1 -1
  29. package/dist-lib/components/voorraad/StepSamenstelling.d.ts +10 -0
  30. package/dist-lib/components/voorraad/StepSamenstelling.d.ts.map +1 -0
  31. package/dist-lib/components/voorraad/StepSamenstelling.js +333 -0
  32. package/dist-lib/components/voorraad/StockBulkButtons.d.ts +1 -1
  33. package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -1
  34. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +1 -1
  35. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
  36. package/dist-lib/components/voorraad/ValidationPage.d.ts +1 -1
  37. package/dist-lib/components/voorraad/ValidationPage.d.ts.map +1 -1
  38. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts +1 -1
  39. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts.map +1 -1
  40. package/dist-lib/components/voorraad/VoorraadPage.d.ts +1 -1
  41. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  42. package/dist-lib/components/voorraad/VoorraadPage.js +14 -3
  43. package/dist-lib/components/voorraad/VoorraadPagination.d.ts +1 -1
  44. package/dist-lib/components/voorraad/VoorraadPagination.d.ts.map +1 -1
  45. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts +1 -1
  46. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
  47. package/dist-lib/components/voorraad/VoorraadTable.d.ts +1 -1
  48. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  49. package/dist-lib/components/voorraad/VoorraadTable.js +6 -13
  50. package/dist-lib/hooks/useFloriday.d.ts +6 -6
  51. package/dist-lib/hooks/useFloriday.d.ts.map +1 -1
  52. package/dist-lib/hooks/useFloridayPriceGroups.d.ts +20 -0
  53. package/dist-lib/hooks/useFloridayPriceGroups.d.ts.map +1 -0
  54. package/dist-lib/hooks/useFloridayPriceGroups.js +55 -0
  55. package/dist-lib/hooks/useFloridayVbnRequired.d.ts +5 -0
  56. package/dist-lib/hooks/useFloridayVbnRequired.d.ts.map +1 -0
  57. package/dist-lib/hooks/useFloridayVbnRequired.js +16 -0
  58. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  59. package/dist-lib/hooks/useLocalTradeItems.js +14 -0
  60. package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -1
  61. package/dist-lib/hooks/useTradeItemSpec.js +48 -1
  62. package/dist-lib/hooks/useTradeItemSupplyLine.d.ts +1 -1
  63. package/dist-lib/hooks/useTradeItemSupplyLine.d.ts.map +1 -1
  64. package/dist-lib/hooks/useVbnRegulatoryFeatures.d.ts +1 -1
  65. package/dist-lib/hooks/useVbnRegulatoryFeatures.d.ts.map +1 -1
  66. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2378 -361
  67. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  68. package/dist-lib/integrations/supabase/client.d.ts +2378 -361
  69. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  70. package/dist-lib/integrations/supabase/client.server.d.ts +2378 -361
  71. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  72. package/dist-lib/integrations/supabase/types.d.ts +2412 -344
  73. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  74. package/dist-lib/integrations/supabase/types.js +11 -9
  75. package/dist-lib/lib/centralSecrets.server.d.ts +21 -0
  76. package/dist-lib/lib/centralSecrets.server.d.ts.map +1 -0
  77. package/dist-lib/lib/centralSecrets.server.js +111 -0
  78. package/dist-lib/lib/floricode-required-features.functions.d.ts +2378 -367
  79. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  80. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +2380 -361
  81. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  82. package/dist-lib/lib/floriday-gateway.functions.d.ts +10760 -675
  83. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  84. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  85. package/dist-lib/lib/floriday-gateway.server.js +34 -9
  86. package/dist-lib/lib/floriday-payload.d.ts +21 -3
  87. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  88. package/dist-lib/lib/floriday-payload.js +167 -34
  89. package/dist-lib/lib/floriday-vbn-required.functions.d.ts +27769 -0
  90. package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -0
  91. package/dist-lib/lib/floriday-vbn-required.functions.js +85 -0
  92. package/dist-lib/lib/floriday-warehouse.functions.d.ts +6542 -491
  93. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  94. package/dist-lib/lib/floriday-writes.functions.d.ts +4647 -612
  95. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  96. package/dist-lib/lib/floriday-writes.functions.js +36 -1
  97. package/dist-lib/lib/trade-item-prices.d.ts +43 -0
  98. package/dist-lib/lib/trade-item-prices.d.ts.map +1 -0
  99. package/dist-lib/lib/trade-item-prices.js +229 -0
  100. package/package.json +3 -3
  101. package/dist-lib/components/settings/VoorraadSettingsCard.d.ts +0 -2
  102. package/dist-lib/components/settings/VoorraadSettingsCard.d.ts.map +0 -1
  103. package/dist-lib/components/settings/VoorraadSettingsCard.js +0 -39
  104. package/dist-lib/components/voorraad/ConnectionPicker.d.ts +0 -8
  105. package/dist-lib/components/voorraad/ConnectionPicker.d.ts.map +0 -1
  106. package/dist-lib/components/voorraad/ConnectionPicker.js +0 -8
@@ -2,26 +2,33 @@ 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";
25
32
  import { FloridayInfoCard } from "../../components/voorraad/FloridayInfoCard";
26
33
  import { stockKeyFor } from "../../hooks/useTradeItemStock";
27
34
  const empty = emptyWizardData;
@@ -71,10 +78,23 @@ function warehouseSubLabel(raw) {
71
78
  const city = addr.city ?? "";
72
79
  return [line, city].filter(Boolean).join(", ") || undefined;
73
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
+ }
74
93
  export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, } = {}) {
75
94
  const navigate = useNavigate();
76
95
  const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
77
96
  const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
97
+ const { priceGroups } = useFloridayPriceGroups(activeId);
78
98
  const [step, setStep] = useState(0);
79
99
  const [data, setData] = useState(empty);
80
100
  const [submitting, setSubmitting] = useState(false);
@@ -89,10 +109,73 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
89
109
  const [productMeta, setProductMeta] = useState({ floridayActive: false, floridayTradeItemId: null, physicalStock: 0 });
90
110
  const [deleteAck, setDeleteAck] = useState(false);
91
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();
92
117
  const isEditingDraft = Boolean(initialDraftId);
93
118
  const isEditingPublished = Boolean(tradeItemId);
94
119
  const isEditingProduct = Boolean(productId);
95
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]);
96
179
  const saveWarehousePreference = useCallback(async (targetTradeItemId, warehouseId) => {
97
180
  if (!activeId || !targetTradeItemId || !warehouseId)
98
181
  return;
@@ -142,6 +225,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
142
225
  .maybeSingle();
143
226
  warehouseId = pref?.warehouse_id ?? warehouseId;
144
227
  }
228
+ if (!warehouseId) {
229
+ warehouseId = (await resolvePreferredWarehouseId()) ?? "";
230
+ }
145
231
  if (cancelled)
146
232
  return;
147
233
  setData({ ...empty, ...row.data, warehouseId });
@@ -155,8 +241,13 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
155
241
  return () => {
156
242
  cancelled = true;
157
243
  };
158
- }, [initialDraftId, activeId]);
244
+ }, [initialDraftId, activeId, resolvePreferredWarehouseId]);
159
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.
160
251
  useEffect(() => {
161
252
  if (!productId)
162
253
  return;
@@ -166,7 +257,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
166
257
  try {
167
258
  const { data: product, error } = await supabase
168
259
  .from("products")
169
- .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")
170
261
  .eq("id", productId)
171
262
  .maybeSingle();
172
263
  if (cancelled)
@@ -184,48 +275,90 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
184
275
  const cpl = Number(product.crates_per_layer) || 0;
185
276
  const lpc = Number(product.layers_per_cart) || 0;
186
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 ?? [];
187
317
  const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
188
- const seededPacking = payloadHasPacking
189
- ? payload.packingConfigurations
190
- : ppc > 0 || cpl > 0 || lpc > 0
191
- ? [
192
- {
193
- id: typeof crypto !== "undefined" && "randomUUID" in crypto
194
- ? crypto.randomUUID()
195
- : `pk-${Date.now()}`,
196
- source: "VBN",
197
- loadCarrier: "DANISH_TROLLEY",
198
- packagingType: "",
199
- unitsPerPackage: ppc || 1,
200
- packagesPerLayer: cpl || 1,
201
- layersPerLoadCarrier: lpc || 1,
202
- isPrimary: true,
203
- isCustomerSpecific: false,
204
- customerOrganizationIds: [],
205
- },
206
- ]
207
- : [];
208
- setData({
209
- ...empty,
210
- ...payload,
211
- name: product.product ?? payload.name ?? "",
212
- articleCode: String(product.article_number ?? payload.articleCode ?? "").trim(),
213
- gtin: validGtin ? barcode : (payload.gtin ?? ""),
214
- salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
215
- saleStockPieces: Number(product.quantity ?? payload.saleStockPieces) || 0,
216
- images: Array.isArray(payload.images) && payload.images.length > 0
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
217
344
  ? payload.images
218
345
  : imageUrl
219
346
  ? [{ url: imageUrl, name: product.product ?? undefined, primary: true }]
220
- : [],
221
- linkedProductId: product.id,
222
- packingConfigurations: seededPacking,
223
- });
224
- setDraftId(product.id);
225
- setDraftFloridayId((product.floriday_trade_item_id ?? null));
226
- const fid = (product.floriday_trade_item_id ?? null);
227
- const fActive = Boolean(product.floriday_active);
228
- // Voorraad ophalen voor deze product-key
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).
229
362
  let physical = 0;
230
363
  try {
231
364
  const { data: stockRow } = await supabase
@@ -236,6 +369,61 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
236
369
  physical = Number(stockRow?.physical_stock ?? 0);
237
370
  }
238
371
  catch { /* stock niet gevonden = 0 */ }
372
+ setData({
373
+ ...empty,
374
+ ...payload,
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 ?? ""),
405
+ salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
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,
422
+ linkedProductId: product.id,
423
+ packingConfigurations: seededPacking,
424
+ });
425
+ setDraftId(product.id);
426
+ setDraftFloridayId(fid);
239
427
  setProductMeta({
240
428
  floridayActive: fActive,
241
429
  floridayTradeItemId: fid,
@@ -254,7 +442,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
254
442
  return () => {
255
443
  cancelled = true;
256
444
  };
257
- }, [productId]);
445
+ }, [productId, activeId, resolvePreferredWarehouseId]);
258
446
  // Load existing published trade item from the shared cache
259
447
  useEffect(() => {
260
448
  if (!tradeItemId || !activeId)
@@ -286,9 +474,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
286
474
  return;
287
475
  }
288
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();
289
482
  setData({
290
483
  ...wizardData,
291
- warehouseId: pref?.warehouse_id ?? wizardData.warehouseId,
484
+ productGroup,
485
+ warehouseId: prefWarehouseId || cachedWarehouseId || preferredWarehouseFallback || "",
292
486
  });
293
487
  setPublishToFloriday(true);
294
488
  }
@@ -304,7 +498,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
304
498
  return () => {
305
499
  cancelled = true;
306
500
  };
307
- }, [tradeItemId, activeId]);
501
+ }, [tradeItemId, activeId, resolvePreferredWarehouseId]);
308
502
  // Floricode data (PascalCase resource namen zoals ze in floricode_records staan)
309
503
  const [vbnProducts, setVbnProducts] = useState([]);
310
504
  const [featureTypes, setFeatureTypes] = useState([]);
@@ -348,7 +542,45 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
348
542
  values: r.values,
349
543
  }));
350
544
  }, [regulatoryQuery.data]);
351
- 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]);
352
584
  // Set van VBN-product-codes die Floriday tijdens write accepteert.
353
585
  // VBN-codes zijn 5- of 6-cijferig en staan in `vbn-product-code` (NIET in
354
586
  // `vbn-product-group`, dat bevat 8-cijferige groeps-ID's).
@@ -372,7 +604,49 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
372
604
  productId: !tradeItemId ? linkedProductId : null,
373
605
  draftId: !tradeItemId && !linkedProductId ? (draftId ?? null) : null,
374
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]);
375
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.
376
650
  const lastPrunedVbnRef = useRef("");
377
651
  useEffect(() => {
378
652
  if (!data.vbnCode)
@@ -381,6 +655,16 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
381
655
  return;
382
656
  if (spec.characteristics.length === 0)
383
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
+ }
384
668
  lastPrunedVbnRef.current = data.vbnCode;
385
669
  const allowedByCode = new Map(spec.characteristics.map((c) => [c.code, c]));
386
670
  setData((d) => {
@@ -401,7 +685,38 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
401
685
  }
402
686
  return { ...d, features: next };
403
687
  });
404
- }, [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]);
405
720
  const validateEigenschappen = (d) => {
406
721
  const hasRegulatoryLookup = /^\d{5,6}$/.test(String(d.vbnCode ?? "").trim());
407
722
  if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
@@ -558,6 +873,35 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
558
873
  toast.error(`Prijs bijwerken mislukt: ${e.message}`);
559
874
  }
560
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
+ }
561
905
  }
562
906
  else if (publishToFloriday) {
563
907
  const payload = buildAddTradeItemPayload(data);
@@ -585,23 +929,36 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
585
929
  // 1) Seed lokale voorraad (physical_stock) — de continuous-stock
586
930
  // trigger pusht dit naar Floriday zodra de trade item in de
587
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.
588
937
  try {
589
938
  const key = stockKeyFor({ tradeItemId });
590
939
  if (key && data.saleStockPieces > 0) {
591
- const userRes = await supabase.auth.getUser();
592
- const userId = userRes.data.user?.id;
593
- if (userId) {
594
- await supabase
595
- .from("trade_item_stock")
596
- .upsert({
597
- user_id: userId,
598
- connection_id: activeId,
599
- trade_item_key: key,
600
- physical_stock: data.saleStockPieces,
601
- minimum_stock: 0,
602
- reserved_stock: 0,
603
- incoming_stock: 0,
604
- }, { 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
+ }
605
962
  }
606
963
  }
607
964
  }
@@ -657,6 +1014,31 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
657
1014
  catch (e) {
658
1015
  toast.error(`Prijs online zetten mislukt: ${e.message}`);
659
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
+ }
660
1042
  }
661
1043
  else {
662
1044
  toast.success("Artikel aangemaakt in Floriday");
@@ -677,7 +1059,27 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
677
1059
  }
678
1060
  catch (e) {
679
1061
  const msg = e.message;
680
- 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")) {
681
1083
  toast.error("Geen actieve Floriday-connectie. Ga naar Instellingen → Floriday.");
682
1084
  }
683
1085
  else {
@@ -742,7 +1144,6 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
742
1144
  // een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
743
1145
  // meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
744
1146
  // het systeem beschikbaar zijn en niet verloren raken bij navigatie.
745
- const lastSavedRef = useRef(null);
746
1147
  useEffect(() => {
747
1148
  if (!activeId)
748
1149
  return;
@@ -752,7 +1153,10 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
752
1153
  if (!draftId && !pushTargetId)
753
1154
  return;
754
1155
  const serialized = JSON.stringify(data);
755
- 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.
756
1160
  lastSavedRef.current = serialized;
757
1161
  return;
758
1162
  }
@@ -766,8 +1170,22 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
766
1170
  if (draftId) {
767
1171
  await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
768
1172
  }
1173
+ if (pushTargetId && !data.productGroup) {
1174
+ setSyncState(draftId ? "local" : "idle");
1175
+ lastSavedRef.current = serialized;
1176
+ return;
1177
+ }
769
1178
  // Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
770
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
+ }
771
1189
  await saveWarehousePreference(pushTargetId, data.warehouseId);
772
1190
  const payload = buildUpdateTradeItemPayload(data);
773
1191
  const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
@@ -830,7 +1248,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
830
1248
  if (!activeConnection) {
831
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." }) }));
832
1250
  }
833
- 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, { open: deleteOpen, onOpenChange: (o) => {
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) => {
834
1252
  setDeleteOpen(o);
835
1253
  if (!o)
836
1254
  setDeleteAck(false);
@@ -860,7 +1278,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
860
1278
  ? "border-primary bg-primary text-primary-foreground"
861
1279
  : status === "invalid"
862
1280
  ? "border-destructive bg-destructive text-destructive-foreground"
863
- : "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
864
1282
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
865
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
866
1284
  ? "Wijzigingen opslaan"
@@ -915,9 +1333,13 @@ function summaryFor(key, d) {
915
1333
  ? `${d.additionalPackaging.length} item(s)`
916
1334
  : "Geen extra verpakking";
917
1335
  case "samenstelling":
918
- return "Binnenkort beschikbaar";
1336
+ return "Basis + per belading";
919
1337
  case "beschikbaarheid":
920
- 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`;
921
1343
  case "aanbieden":
922
1344
  if (!d.offerForSale)
923
1345
  return "Niet aanbieden";
@@ -950,7 +1372,10 @@ function StepBody(p) {
950
1372
  case "verpakking":
951
1373
  return _jsx(StepVerpakking, { ...p });
952
1374
  case "samenstelling":
953
- 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
+ } }));
954
1379
  case "beschikbaarheid":
955
1380
  return _jsx(StepBeschikbaarheid, { ...p });
956
1381
  case "aanbieden":
@@ -985,11 +1410,11 @@ function groupOfRecord(r) {
985
1410
  const n = Number(raw);
986
1411
  if (Number.isFinite(n) && n > 0) {
987
1412
  const s = String(Math.floor(n));
988
- if (s.startsWith("10"))
1413
+ if (s.startsWith("10") || s.startsWith("11"))
989
1414
  return "CUT_FLOWER";
990
- if (s.startsWith("20"))
1415
+ if (s.startsWith("20") || s.startsWith("21") || s.startsWith("29"))
991
1416
  return "POT_PLANT";
992
- if (s.startsWith("30"))
1417
+ if (s.startsWith("30") || s.startsWith("31"))
993
1418
  return "GARDEN_PLANT";
994
1419
  return null;
995
1420
  }
@@ -1086,8 +1511,12 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
1086
1511
  setQuery("");
1087
1512
  };
1088
1513
  const triggerLabel = data.vbnName
1089
- ? data.vbnName
1090
- : "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";
1091
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) => {
1092
1521
  const next = v === "ALL" ? null : v;
1093
1522
  update("productGroup", next);
@@ -1122,7 +1551,7 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
1122
1551
  function InfoTip({ text }) {
1123
1552
  return (_jsx("span", { title: text, className: "inline-flex cursor-help text-muted-foreground", children: _jsx(Info, { className: "h-3.5 w-3.5" }) }));
1124
1553
  }
1125
- function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
1554
+ function StepArtikel({ data, update, collectionsList, setCollectionsList, articleCodeLocked }) {
1126
1555
  const [newCollection, setNewCollection] = useState("");
1127
1556
  const [showNewCollection, setShowNewCollection] = useState(false);
1128
1557
  const addCollection = async (name) => {
@@ -1152,7 +1581,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
1152
1581
  icon: ClipboardList,
1153
1582
  },
1154
1583
  ];
1155
- 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) => {
1156
1585
  if (v === "__new__") {
1157
1586
  setShowNewCollection(true);
1158
1587
  return;
@@ -1174,7 +1603,7 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList }) {
1174
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: () => {
1175
1604
  setShowNewCollection(false);
1176
1605
  setNewCollection("");
1177
- }, 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) => {
1178
1607
  const Icon = t.icon;
1179
1608
  const active = data.articleType === t.value;
1180
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
@@ -1237,7 +1666,10 @@ function StepPaspoort({ data, update }) {
1237
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) => {
1238
1667
  const checked = selectedCountries.includes(c.code);
1239
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));
1240
- })] })] })] })] })] }));
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
+ }) }))] })] }));
1241
1673
  }
1242
1674
  function StepEigenschappen({ data, update, spec, schemaQuery, regulatoryQuery }) {
1243
1675
  if (!data.vbnCode) {
@@ -1295,6 +1727,11 @@ function EigenschappenBody({ data, update, spec, }) {
1295
1727
  const hasValue = (code) => Boolean((data.features[code] ?? "").trim());
1296
1728
  const visibleFields = spec.characteristics.filter((c) => c.required || visibleOptional.has(c.code) || hasValue(c.code));
1297
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));
1298
1735
  const filteredHidden = hiddenOptional.filter((c) => {
1299
1736
  if (!addSearch.trim())
1300
1737
  return true;
@@ -1321,7 +1758,13 @@ function EigenschappenBody({ data, update, spec, }) {
1321
1758
  delete nextFeatures[code];
1322
1759
  update("features", nextFeatures);
1323
1760
  };
1324
- 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)
1325
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
1326
1769
  ? "Alle beschikbare karakteristieken zijn al toegevoegd."
1327
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)))) })] })] }) })] }));
@@ -1502,7 +1945,34 @@ function StepFotos({ data, update, activeId }) {
1502
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) => {
1503
1946
  onFiles(e.target.files);
1504
1947
  e.target.value = "";
1505
- } })] }), 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
+ }) }))] }));
1506
1976
  }
1507
1977
  /**
1508
1978
  * Warehouse-stap — verplicht in Floriday Suppliers API 2026v1.
@@ -1601,12 +2071,12 @@ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCu
1601
2071
  const editing = editingId
1602
2072
  ? data.packingConfigurations.find((p) => p.id === editingId) ?? null
1603
2073
  : null;
1604
- 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) => {
1605
2075
  const pkgLabel = packagingTypes.find((r) => codeOf(r) === p.packagingType);
1606
2076
  const lcLabel = loadCarriers.find((r) => codeOf(r) === p.loadCarrier);
1607
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
1608
2078
  ? `${codeOf(pkgLabel)}: ${labelOf(pkgLabel)}`
1609
- : 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));
1610
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) => {
1611
2081
  setDialogOpen(o);
1612
2082
  if (!o)
@@ -1628,6 +2098,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1628
2098
  isCustomerSpecific: false,
1629
2099
  customerOrganizationIds: [],
1630
2100
  isPrimary: isFirst,
2101
+ isHiddenForDirectSales: false,
1631
2102
  });
1632
2103
  const [draft, setDraft] = useState(emptyItem);
1633
2104
  const [pkgSearch, setPkgSearch] = useState("");
@@ -1657,8 +2128,16 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1657
2128
  ? draft.customerOrganizationIds.filter((x) => x !== id)
1658
2129
  : [...draft.customerOrganizationIds, id]);
1659
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;
1660
2138
  const canSave = draft.loadCarrier.length > 0 &&
1661
- draft.packagingType.length > 0 &&
2139
+ (hasCustomPkg || draft.packagingType.length > 0) &&
2140
+ !materialInvalid &&
1662
2141
  (draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
1663
2142
  (!draft.isCustomerSpecific || draft.customerOrganizationIds.length > 0);
1664
2143
  const onUploadPhoto = async (file) => {
@@ -1695,14 +2174,22 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1695
2174
  ? "border-primary bg-primary/5"
1696
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"
1697
2176
  ? "border-primary bg-primary/5"
1698
- : "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) => {
1699
- const sup = PACKAGING_SUPPLIERS.find((s) => s.id === v);
1700
- setDraft((d) => ({
1701
- ...d,
1702
- packagingSupplierId: v,
1703
- packagingSupplierName: sup?.name,
1704
- }));
1705
- }, 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
1706
2193
  ? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
1707
2194
  ? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
1708
2195
  : draft.packagingType)
@@ -1714,7 +2201,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1714
2201
  photoUrl: undefined,
1715
2202
  photoImageId: undefined,
1716
2203
  photoPreviewUrl: undefined,
1717
- })), 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" })] })] }) }));
1718
2205
  }
1719
2206
  function StepVerpakking({ data, update }) {
1720
2207
  const [dialogOpen, setDialogOpen] = useState(false);
@@ -1723,14 +2210,14 @@ function StepVerpakking({ data, update }) {
1723
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 })] }));
1724
2211
  }
1725
2212
  function StepBeschikbaarheid({ data, update }) {
1726
- const weeks = Array.from({ length: 52 }, (_, i) => i + 1);
2213
+ const weeks = Array.from({ length: 53 }, (_, i) => i + 1);
1727
2214
  // Drag-selectie: bij mousedown bepalen we of we in "select"- of "deselect"-
1728
2215
  // modus zitten (op basis van de startcel) en breiden we de selectie uit
1729
2216
  // terwijl de gebruiker over de weken sleept, zoals in Floriday.
1730
2217
  const dragStateRef = useRef(null);
1731
2218
  const [dragHover, setDragHover] = useState(null);
1732
2219
  const setSelection = (list) => {
1733
- 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)))
1734
2221
  .sort((a, b) => a - b);
1735
2222
  update("weeks", unique);
1736
2223
  };
@@ -1836,7 +2323,14 @@ function StepBeschikbaarheid({ data, update }) {
1836
2323
  pointerXRef.current = e.clientX;
1837
2324
  startAutoScrollLoop();
1838
2325
  };
1839
- 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) => {
1840
2334
  const selected = data.weeks.includes(w);
1841
2335
  const isHover = dragHover === w;
1842
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
@@ -1847,18 +2341,56 @@ function StepBeschikbaarheid({ data, update }) {
1847
2341
  // ------------------------------------------------------------
1848
2342
  // Stap "Aanbieden & voorraad" — Direct Sale via CustomerOffer
1849
2343
  // ------------------------------------------------------------
1850
- function StepAanbieden({ data, update }) {
2344
+ function StepAanbieden({ data, update, priceGroups }) {
1851
2345
  const priceStr = Number.isFinite(data.salePricePerPiece)
1852
2346
  ? String(data.salePricePerPiece)
1853
2347
  : "";
1854
2348
  const stockStr = Number.isFinite(data.saleStockPieces)
1855
2349
  ? String(data.saleStockPieces)
1856
2350
  : "";
1857
- 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
1858
2363
  ? "Aanbod wordt aangemaakt direct na publiceren."
1859
- : "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
1860
- ? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
1861
- : "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)))] })] })] })] }));
1862
2394
  }
1863
2395
  // ============================================================
1864
2396
  // Shared