@flowselections/floriday-voorraad 1.0.15 → 1.0.17

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 (84) hide show
  1. package/dist-lib/components/voorraad/AdditionalPackagingDialog.d.ts +9 -0
  2. package/dist-lib/components/voorraad/AdditionalPackagingDialog.d.ts.map +1 -0
  3. package/dist-lib/components/voorraad/AdditionalPackagingDialog.js +51 -0
  4. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  5. package/dist-lib/components/voorraad/ArtikelWizard.js +411 -28
  6. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
  7. package/dist-lib/components/voorraad/EditArtikelPage.js +78 -4
  8. package/dist-lib/components/voorraad/ExcelExportButton.d.ts +2 -1
  9. package/dist-lib/components/voorraad/ExcelExportButton.d.ts.map +1 -1
  10. package/dist-lib/components/voorraad/ExcelExportButton.js +24 -5
  11. package/dist-lib/components/voorraad/OrderHistoryCard.d.ts +8 -0
  12. package/dist-lib/components/voorraad/OrderHistoryCard.d.ts.map +1 -0
  13. package/dist-lib/components/voorraad/OrderHistoryCard.js +133 -0
  14. package/dist-lib/components/voorraad/PriceStatusBadge.d.ts +7 -0
  15. package/dist-lib/components/voorraad/PriceStatusBadge.d.ts.map +1 -0
  16. package/dist-lib/components/voorraad/PriceStatusBadge.js +67 -0
  17. package/dist-lib/components/voorraad/ProductLinkCard.d.ts +13 -0
  18. package/dist-lib/components/voorraad/ProductLinkCard.d.ts.map +1 -0
  19. package/dist-lib/components/voorraad/ProductLinkCard.js +159 -0
  20. package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -1
  21. package/dist-lib/components/voorraad/StockBulkButtons.js +88 -7
  22. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts +2 -1
  23. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
  24. package/dist-lib/components/voorraad/VoorraadStockCard.js +5 -4
  25. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  26. package/dist-lib/components/voorraad/VoorraadTable.js +6 -1
  27. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  28. package/dist-lib/hooks/useLocalTradeItems.js +4 -0
  29. package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -1
  30. package/dist-lib/hooks/useTradeItemSpec.js +4 -1
  31. package/dist-lib/hooks/useTradeItemStock.d.ts +14 -1
  32. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
  33. package/dist-lib/hooks/useTradeItemStock.js +76 -17
  34. package/dist-lib/hooks/useTradeItemSupplyLine.d.ts +21 -0
  35. package/dist-lib/hooks/useTradeItemSupplyLine.d.ts.map +1 -0
  36. package/dist-lib/hooks/useTradeItemSupplyLine.js +49 -0
  37. package/dist-lib/hooks/useVoorraadData.d.ts +5 -4
  38. package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
  39. package/dist-lib/hooks/useVoorraadData.js +73 -7
  40. package/dist-lib/index.d.ts +1 -0
  41. package/dist-lib/index.d.ts.map +1 -1
  42. package/dist-lib/index.js +1 -0
  43. package/dist-lib/integrations/supabase/auth-middleware.d.ts +1307 -94
  44. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  45. package/dist-lib/integrations/supabase/client.d.ts +1307 -94
  46. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  47. package/dist-lib/integrations/supabase/client.server.d.ts +1307 -94
  48. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  49. package/dist-lib/integrations/supabase/types.d.ts +1328 -87
  50. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  51. package/dist-lib/lib/ensure-article-codes.d.ts +8 -0
  52. package/dist-lib/lib/ensure-article-codes.d.ts.map +1 -0
  53. package/dist-lib/lib/ensure-article-codes.js +78 -0
  54. package/dist-lib/lib/floricode-required-features.functions.d.ts +1313 -94
  55. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  56. package/dist-lib/lib/floricode-required-features.functions.js +20 -2
  57. package/dist-lib/lib/floriday-client.d.ts +6 -0
  58. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  59. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +11264 -0
  60. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -0
  61. package/dist-lib/lib/floriday-customer-offer.functions.js +175 -0
  62. package/dist-lib/lib/floriday-gateway.functions.d.ts +16129 -17
  63. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  64. package/dist-lib/lib/floriday-gateway.functions.js +150 -0
  65. package/dist-lib/lib/floriday-gateway.server.d.ts +22 -0
  66. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  67. package/dist-lib/lib/floriday-gateway.server.js +3 -0
  68. package/dist-lib/lib/floriday-payload.d.ts +76 -8
  69. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  70. package/dist-lib/lib/floriday-payload.js +209 -39
  71. package/dist-lib/lib/floriday-supply-line.d.ts +53 -0
  72. package/dist-lib/lib/floriday-supply-line.d.ts.map +1 -0
  73. package/dist-lib/lib/floriday-supply-line.js +109 -0
  74. package/dist-lib/lib/floriday-warehouse.functions.d.ts +33638 -0
  75. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -0
  76. package/dist-lib/lib/floriday-warehouse.functions.js +98 -0
  77. package/dist-lib/lib/floriday-writes.functions.d.ts +2707 -281
  78. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  79. package/dist-lib/lib/floriday-writes.functions.js +39 -6
  80. package/dist-lib/lib/vrs-catalog.d.ts +27 -0
  81. package/dist-lib/lib/vrs-catalog.d.ts.map +1 -0
  82. package/dist-lib/lib/vrs-catalog.js +98 -0
  83. package/package.json +3 -2
  84. package/public/flowselections-assets/Voorraad/floriday-logo.png +0 -0
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef, useState } from "react";
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
5
  import { ArrowLeft, ArrowRight, Check, ChevronDown, ClipboardList, Info, Loader2, Plus, Search, Trash2, Upload, UploadCloud, Users, X, } from "lucide-react";
@@ -8,14 +8,29 @@ import { useFlorydayCustomers } from "../../hooks/useFlorydayCustomers";
8
8
  import { floriday } from "../../lib/floriday-client";
9
9
  import { listFloricode } from "../../lib/floricode-client";
10
10
  import { supabase } from "../../integrations/supabase/client";
11
- import { buildAddTradeItemPayload, emptyWizardData, floridayCacheToWizardData, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
11
+ import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, buildPrimaryPackingBase, emptyWizardData, floridayCacheToWizardData, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
12
+ import { AdditionalPackagingDialog } from "./AdditionalPackagingDialog";
13
+ import { publishCustomerOfferFn } from "../../lib/floriday-customer-offer.functions";
14
+ import { upsertSupplyLineForTradeItem } from "../../lib/floriday-supply-line";
12
15
  import { deleteLocalTradeItem, getLocalTradeItem, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
13
16
  import { useTradeItemSpec } from "../../hooks/useTradeItemSpec";
14
17
  import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures";
15
18
  import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday";
16
- import floridayLogo from "../../assets/floriday-logo.png.asset.json";
19
+ const floridayLogo = "/flowselections-assets/Voorraad/floriday-logo.png";
17
20
  import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
21
+ import { ProductLinkCard } from "../../components/voorraad/ProductLinkCard";
22
+ import { PriceStatusBadge } from "../../components/voorraad/PriceStatusBadge";
23
+ import { OrderHistoryCard } from "../../components/voorraad/OrderHistoryCard";
24
+ import { stockKeyFor } from "../../hooks/useTradeItemStock";
18
25
  const empty = emptyWizardData;
26
+ const LOAD_CARRIER_LABELS = {
27
+ AUCTION_TROLLEY: "Stapelwagen",
28
+ DANISH_TROLLEY: "Deense container",
29
+ EURO_TROLLEY: "Euro-rolcontainer",
30
+ PALLET: "Pallet",
31
+ EURO_PALLET: "Euro-pallet",
32
+ NONE: "Geen ladingdrager",
33
+ };
19
34
  const STEPS = [
20
35
  { key: "vbn", label: "VBN-code" },
21
36
  { key: "artikel", label: "Artikel" },
@@ -23,10 +38,12 @@ const STEPS = [
23
38
  { key: "eigenschappen", label: "Eigenschappen" },
24
39
  { key: "klanten", label: "Klanten" },
25
40
  { key: "fotos", label: "Foto's" },
41
+ { key: "warehouse", label: "Warehouse" },
26
42
  { key: "beladingen", label: "Beladingen" },
27
43
  { key: "verpakking", label: "Aanvullende verpakkingsmaterialen" },
28
44
  { key: "samenstelling", label: "Samenstelling", disabled: true },
29
45
  { key: "beschikbaarheid", label: "Verwachte beschikbaarheid" },
46
+ { key: "aanbieden", label: "Aanbieden & voorraad" },
30
47
  ];
31
48
  function visibleSteps(d) {
32
49
  return STEPS.filter((s) => {
@@ -36,6 +53,20 @@ function visibleSteps(d) {
36
53
  return true;
37
54
  });
38
55
  }
56
+ function warehouseDisplayLabel(raw, fallback) {
57
+ const addr = raw?.location?.address ?? raw?.address ?? {};
58
+ const line = addr.addressLine ?? [addr.street, addr.houseNumber].filter(Boolean).join(" ");
59
+ const city = addr.city ?? "";
60
+ return ((raw?.name && String(raw.name).trim()) ||
61
+ [line, city].filter(Boolean).join(", ") ||
62
+ fallback);
63
+ }
64
+ function warehouseSubLabel(raw) {
65
+ const addr = raw?.location?.address ?? raw?.address ?? {};
66
+ const line = addr.addressLine ?? [addr.street, addr.houseNumber].filter(Boolean).join(" ");
67
+ const city = addr.city ?? "";
68
+ return [line, city].filter(Boolean).join(", ") || undefined;
69
+ }
39
70
  export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
40
71
  const navigate = useNavigate();
41
72
  const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
@@ -45,11 +76,43 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
45
76
  const [submitting, setSubmitting] = useState(false);
46
77
  const [publishToFloriday, setPublishToFloriday] = useState(true);
47
78
  const [draftId, setDraftId] = useState(initialDraftId);
79
+ const [draftFloridayId, setDraftFloridayId] = useState(null);
48
80
  const [draftLoading, setDraftLoading] = useState(Boolean(initialDraftId));
49
81
  const [publishedLoading, setPublishedLoading] = useState(Boolean(tradeItemId));
82
+ const [syncState, setSyncState] = useState("idle");
83
+ const [syncError, setSyncError] = useState(null);
50
84
  const isEditingDraft = Boolean(initialDraftId);
51
85
  const isEditingPublished = Boolean(tradeItemId);
52
86
  const isEditing = isEditingDraft || isEditingPublished;
87
+ const saveWarehousePreference = useCallback(async (targetTradeItemId, warehouseId) => {
88
+ if (!activeId || !targetTradeItemId || !warehouseId)
89
+ return;
90
+ let warehouseName = null;
91
+ try {
92
+ const { data: warehouseRow } = await supabase
93
+ .from("floriday_warehouses_cache")
94
+ .select("data")
95
+ .eq("connection_id", activeId)
96
+ .eq("floriday_id", warehouseId)
97
+ .maybeSingle();
98
+ if (warehouseRow?.data) {
99
+ warehouseName = warehouseDisplayLabel(warehouseRow.data, warehouseId);
100
+ }
101
+ }
102
+ catch {
103
+ // Naam is alleen voor herkenbaarheid; de id is leidend.
104
+ }
105
+ const { error } = await supabase
106
+ .from("trade_item_warehouse_preferences")
107
+ .upsert({
108
+ connection_id: activeId,
109
+ trade_item_id: targetTradeItemId,
110
+ warehouse_id: warehouseId,
111
+ warehouse_name: warehouseName,
112
+ }, { onConflict: "connection_id,trade_item_id" });
113
+ if (error)
114
+ throw new Error(`Voorraadlocatie opslaan mislukt: ${error.message}`);
115
+ }, [activeId]);
53
116
  // Load existing draft when editing
54
117
  useEffect(() => {
55
118
  if (!initialDraftId)
@@ -57,10 +120,23 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
57
120
  let cancelled = false;
58
121
  setDraftLoading(true);
59
122
  getLocalTradeItem(initialDraftId)
60
- .then((row) => {
123
+ .then(async (row) => {
61
124
  if (cancelled || !row)
62
125
  return;
63
- setData({ ...empty, ...row.data });
126
+ let warehouseId = row.data.warehouseId;
127
+ if (activeId && row.floriday_id) {
128
+ const { data: pref } = await supabase
129
+ .from("trade_item_warehouse_preferences")
130
+ .select("warehouse_id")
131
+ .eq("connection_id", activeId)
132
+ .eq("trade_item_id", row.floriday_id)
133
+ .maybeSingle();
134
+ warehouseId = pref?.warehouse_id ?? warehouseId;
135
+ }
136
+ if (cancelled)
137
+ return;
138
+ setData({ ...empty, ...row.data, warehouseId });
139
+ setDraftFloridayId(row.floriday_id ?? null);
64
140
  })
65
141
  .catch((e) => toast.error(`Lokaal artikel laden mislukt: ${e.message}`))
66
142
  .finally(() => {
@@ -70,7 +146,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
70
146
  return () => {
71
147
  cancelled = true;
72
148
  };
73
- }, [initialDraftId]);
149
+ }, [initialDraftId, activeId]);
74
150
  // Load existing published trade item from the shared cache
75
151
  useEffect(() => {
76
152
  if (!tradeItemId || !activeId)
@@ -85,6 +161,12 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
85
161
  .eq("connection_id", activeId)
86
162
  .eq("floriday_id", tradeItemId)
87
163
  .maybeSingle();
164
+ const { data: pref } = await supabase
165
+ .from("trade_item_warehouse_preferences")
166
+ .select("warehouse_id")
167
+ .eq("connection_id", activeId)
168
+ .eq("trade_item_id", tradeItemId)
169
+ .maybeSingle();
88
170
  if (cancelled)
89
171
  return;
90
172
  if (error) {
@@ -95,7 +177,11 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
95
177
  toast.error("Artikel niet gevonden in de cache");
96
178
  return;
97
179
  }
98
- setData(floridayCacheToWizardData(row.data));
180
+ const wizardData = floridayCacheToWizardData(row.data);
181
+ setData({
182
+ ...wizardData,
183
+ warehouseId: pref?.warehouse_id ?? wizardData.warehouseId,
184
+ });
99
185
  setPublishToFloriday(true);
100
186
  }
101
187
  catch (e) {
@@ -120,8 +206,16 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
120
206
  useEffect(() => {
121
207
  // Kleine resources volledig laden
122
208
  listFloricode("FeatureType", undefined, 300).then(setFeatureTypes).catch(() => { });
123
- listFloricode("PackagingType", undefined, 100).then(setPackagingTypes).catch(() => { });
124
- listFloricode("LoadCarrier", undefined, 600).then(setLoadCarriers).catch(() => { });
209
+ // Fust/VBN-package: Floricode-resource `LoadCarrier` (=/v2/VBN/Packaging, ~560 rijen).
210
+ // `PackagingType` (33 rijen) zijn slechts categorieën (Kraag/Deksel/…) en NIET wat
211
+ // Floriday in `package.vbnPackageCode` valideert — dat leverde 400 "Package type code X
212
+ // not found in floricode data" op. Zelfde lijst voor fust-picker en label-lookup.
213
+ listFloricode("LoadCarrier", undefined, 1000)
214
+ .then((rows) => {
215
+ setLoadCarriers(rows);
216
+ setPackagingTypes(rows);
217
+ })
218
+ .catch(() => { });
125
219
  // VbnProduct (51k records) wordt server-side per productgroep geladen in StepVbn.
126
220
  supabase
127
221
  .from("collections")
@@ -164,6 +258,12 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
164
258
  }
165
259
  return set.size ? set : null;
166
260
  }, [schemaQuery.data]);
261
+ const linkedProductId = typeof data.linkedProductId === "string" ? data.linkedProductId : null;
262
+ const localTradeItemKey = stockKeyFor({
263
+ tradeItemId: tradeItemId ?? null,
264
+ productId: !tradeItemId ? linkedProductId : null,
265
+ draftId: !tradeItemId && !linkedProductId ? (draftId ?? null) : null,
266
+ });
167
267
  // Bij wissel van VBN: gooi features weg die niet meer bij het schema horen.
168
268
  const lastPrunedVbnRef = useRef("");
169
269
  useEffect(() => {
@@ -299,17 +399,26 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
299
399
  setSubmitting(true);
300
400
  try {
301
401
  if (isEditingPublished && tradeItemId) {
402
+ await saveWarehousePreference(tradeItemId, data.warehouseId);
302
403
  // PUT vervangt het bestaande trade item in Floriday
303
- const payload = { ...buildAddTradeItemPayload(data), tradeItemId };
404
+ // PUT /trade-items/{id} — body volgens TradeItemUpdate (zonder tradeItemId + supplierArticleCode)
405
+ const payload = buildUpdateTradeItemPayload(data);
304
406
  const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
305
407
  if (!res?.attemptId) {
306
408
  throw new Error("Gateway gaf geen attemptId terug — wijziging niet bevestigd");
307
409
  }
308
410
  // Optimistisch: cache-rij meteen bijwerken zodat de tabel niet 5 min wacht
309
411
  try {
412
+ const cachePayload = {
413
+ ...buildAddTradeItemPayload(data),
414
+ tradeItemId,
415
+ };
310
416
  await supabase
311
417
  .from("floriday_trade_items_cache")
312
- .update({ data: payload, updated_at: new Date().toISOString() })
418
+ .update({
419
+ data: cachePayload,
420
+ fetched_at: new Date().toISOString(),
421
+ })
313
422
  .eq("connection_id", activeId)
314
423
  .eq("floriday_id", tradeItemId);
315
424
  }
@@ -317,6 +426,30 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
317
426
  /* niet kritiek, sync corrigeert binnen 5 min */
318
427
  }
319
428
  toast.success("Artikel bijgewerkt in Floriday");
429
+ // Herprijzen: als er een prijs is ingevuld, supply-line (opnieuw) pushen.
430
+ if (data.offerForSale && data.salePricePerPiece > 0) {
431
+ try {
432
+ const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
433
+ data.packingConfigurations[0];
434
+ if (primary?.id) {
435
+ await upsertSupplyLineForTradeItem({
436
+ connectionId: activeId,
437
+ tradeItemId,
438
+ pricePerPiece: data.salePricePerPiece,
439
+ packingConfigurationId: primary.id,
440
+ availableQuantity: data.saleStockPieces,
441
+ customerOrganizationIds: data.customerOrganizationIds,
442
+ periodStart: data.offerStartDate
443
+ ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
444
+ : undefined,
445
+ });
446
+ toast.success("Prijs bijgewerkt in Floriday-catalogus");
447
+ }
448
+ }
449
+ catch (e) {
450
+ toast.error(`Prijs bijwerken mislukt: ${e.message}`);
451
+ }
452
+ }
320
453
  }
321
454
  else if (publishToFloriday) {
322
455
  const payload = buildAddTradeItemPayload(data);
@@ -324,6 +457,8 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
324
457
  if (!res?.attemptId) {
325
458
  throw new Error("Gateway gaf geen attemptId terug — aanmaken niet bevestigd");
326
459
  }
460
+ const createdTradeItemId = res.tradeItemId ?? payload.tradeItemId;
461
+ await saveWarehousePreference(createdTradeItemId, data.warehouseId);
327
462
  // Als dit een lokaal artikel was, ruim de draft-rij op
328
463
  if (draftId) {
329
464
  try {
@@ -333,7 +468,91 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
333
468
  // Niet kritiek; rij verdwijnt na volgende reload
334
469
  }
335
470
  }
336
- toast.success("Artikel aangemaakt in Floriday");
471
+ // ---- Direct-sale flow -----------------------------------------
472
+ // Alleen doorzetten als de gebruiker de "Aanbieden"-stap geactiveerd
473
+ // heeft. Falen mag de TradeItem-succes NIET terugdraaien — daarom
474
+ // in een aparte try/catch en apart toast bericht.
475
+ if (data.offerForSale) {
476
+ const tradeItemId = createdTradeItemId;
477
+ // 1) Seed lokale voorraad (physical_stock) — de continuous-stock
478
+ // trigger pusht dit naar Floriday zodra de trade item in de
479
+ // cache verschijnt.
480
+ try {
481
+ const key = stockKeyFor({ tradeItemId });
482
+ if (key && data.saleStockPieces > 0) {
483
+ const userRes = await supabase.auth.getUser();
484
+ const userId = userRes.data.user?.id;
485
+ if (userId) {
486
+ await supabase
487
+ .from("trade_item_stock")
488
+ .upsert({
489
+ user_id: userId,
490
+ connection_id: activeId,
491
+ trade_item_key: key,
492
+ physical_stock: data.saleStockPieces,
493
+ minimum_stock: 0,
494
+ reserved_stock: 0,
495
+ incoming_stock: 0,
496
+ }, { onConflict: "connection_id,trade_item_key" });
497
+ }
498
+ }
499
+ }
500
+ catch (e) {
501
+ toast.error(`Voorraad opslaan mislukt: ${e.message}`);
502
+ }
503
+ // 2) CustomerOffer publiceren (POST /customer-offers)
504
+ try {
505
+ const packingBase = buildPrimaryPackingBase(data);
506
+ await publishCustomerOfferFn({
507
+ data: {
508
+ customerOfferId: crypto.randomUUID(),
509
+ tradeItemId,
510
+ title: data.name || data.vbnName || "Aanbod",
511
+ description: "",
512
+ pricePerPiece: data.salePricePerPiece,
513
+ currency: "EUR",
514
+ despatchWarehouseId: data.warehouseId,
515
+ numberOfPieces: data.saleStockPieces,
516
+ allowedCustomerOrganizationIds: data.customerOrganizationIds,
517
+ packingConfiguration: packingBase,
518
+ orderStart: new Date(`${data.offerStartDate}T00:00:00Z`).toISOString(),
519
+ },
520
+ });
521
+ toast.success("Artikel aangemaakt en aangeboden in Floriday");
522
+ }
523
+ catch (e) {
524
+ toast.error(`Aanbod publiceren mislukt: ${e.message}`);
525
+ }
526
+ // 3) Supply-line pushen naar de Authenticatie-module gateway
527
+ // zodat het product met prijs online komt in de Floriday-catalogus.
528
+ // Idempotent op supplyLineId — herprijzen hergebruikt dezelfde regel.
529
+ try {
530
+ const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
531
+ data.packingConfigurations[0];
532
+ if (primary?.id && data.salePricePerPiece > 0) {
533
+ const sl = await upsertSupplyLineForTradeItem({
534
+ connectionId: activeId,
535
+ tradeItemId,
536
+ pricePerPiece: data.salePricePerPiece,
537
+ packingConfigurationId: primary.id,
538
+ availableQuantity: data.saleStockPieces,
539
+ customerOrganizationIds: data.customerOrganizationIds,
540
+ periodStart: data.offerStartDate
541
+ ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
542
+ : undefined,
543
+ });
544
+ if (sl.supplyLineId) {
545
+ toast.success("Prijs online gezet in Floriday-catalogus");
546
+ }
547
+ }
548
+ }
549
+ catch (e) {
550
+ toast.error(`Prijs online zetten mislukt: ${e.message}`);
551
+ }
552
+ }
553
+ else {
554
+ toast.success("Artikel aangemaakt in Floriday");
555
+ }
337
556
  }
338
557
  else {
339
558
  const newId = await upsertLocalTradeItem({
@@ -379,7 +598,11 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
379
598
  toast.error("Geen actieve Floriday-connectie geselecteerd");
380
599
  return;
381
600
  }
382
- if (!vbnValid) {
601
+ // Voor een nieuw concept blijft een VBN-code de minimale poort. Voor
602
+ // bestaande concepten (draftId aanwezig) mag ALLES wat de gebruiker
603
+ // wijzigt (o.a. beladingen) opgeslagen worden — anders raken tussentijdse
604
+ // bewerkingen verloren zodra ze terug navigeren.
605
+ if (!draftId && !vbnValid) {
383
606
  toast.error("Kies eerst een VBN-code voordat je een concept opslaat");
384
607
  setStep(0);
385
608
  return;
@@ -391,6 +614,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
391
614
  connectionId: activeId,
392
615
  data,
393
616
  });
617
+ if (draftFloridayId && data.warehouseId) {
618
+ await saveWarehousePreference(draftFloridayId, data.warehouseId);
619
+ }
394
620
  setDraftId(newId);
395
621
  toast.success(draftId ? "Concept bijgewerkt" : "Concept opgeslagen");
396
622
  navigate({ to: "/voorraad" });
@@ -402,6 +628,87 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
402
628
  setSubmitting(false);
403
629
  }
404
630
  };
631
+ // Auto-save: zodra er een bestaand concept OF een gepubliceerd artikel in
632
+ // bewerking is, persisteren we wijzigingen debounced. Voor concepten gaan de
633
+ // wijzigingen naar `trade_item_drafts`; voor gepubliceerde artikelen wordt
634
+ // een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
635
+ // meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
636
+ // het systeem beschikbaar zijn en niet verloren raken bij navigatie.
637
+ const lastSavedRef = useRef(null);
638
+ useEffect(() => {
639
+ if (!activeId)
640
+ return;
641
+ if (draftLoading || publishedLoading)
642
+ return;
643
+ const pushTargetId = (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null);
644
+ if (!draftId && !pushTargetId)
645
+ return;
646
+ const serialized = JSON.stringify(data);
647
+ if (lastSavedRef.current === null) {
648
+ lastSavedRef.current = serialized;
649
+ return;
650
+ }
651
+ if (lastSavedRef.current === serialized)
652
+ return;
653
+ const t = window.setTimeout(async () => {
654
+ setSyncState("saving");
655
+ setSyncError(null);
656
+ try {
657
+ // Altijd lokaal opslaan als we een draft-rij hebben
658
+ if (draftId) {
659
+ await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
660
+ }
661
+ // Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
662
+ if (pushTargetId) {
663
+ await saveWarehousePreference(pushTargetId, data.warehouseId);
664
+ const payload = buildUpdateTradeItemPayload(data);
665
+ const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
666
+ if (!res?.attemptId) {
667
+ throw new Error("Gateway gaf geen attemptId terug");
668
+ }
669
+ try {
670
+ const cachePayload = {
671
+ ...buildAddTradeItemPayload(data),
672
+ tradeItemId: pushTargetId,
673
+ };
674
+ await supabase
675
+ .from("floriday_trade_items_cache")
676
+ .update({
677
+ data: cachePayload,
678
+ fetched_at: new Date().toISOString(),
679
+ })
680
+ .eq("connection_id", activeId)
681
+ .eq("floriday_id", pushTargetId);
682
+ }
683
+ catch {
684
+ /* niet kritiek; sync corrigeert */
685
+ }
686
+ setSyncState("saved");
687
+ }
688
+ else {
689
+ setSyncState("local");
690
+ }
691
+ lastSavedRef.current = serialized;
692
+ }
693
+ catch (e) {
694
+ const msg = e.message || "Onbekende fout";
695
+ setSyncError(msg);
696
+ setSyncState("error");
697
+ toast.error(`Sync naar Floriday mislukt: ${msg}`);
698
+ }
699
+ }, 800);
700
+ return () => window.clearTimeout(t);
701
+ }, [
702
+ data,
703
+ draftId,
704
+ draftFloridayId,
705
+ activeId,
706
+ draftLoading,
707
+ publishedLoading,
708
+ isEditingPublished,
709
+ saveWarehousePreference,
710
+ tradeItemId,
711
+ ]);
405
712
  if (connLoading || draftLoading || publishedLoading) {
406
713
  return (_jsx("main", { className: "p-6", children: _jsxs(Card, { className: "p-6 flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), publishedLoading
407
714
  ? "Artikel laden..."
@@ -412,7 +719,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
412
719
  if (!activeConnection) {
413
720
  return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-connectie gevonden. Voeg er eerst een toe in Instellingen." }) }));
414
721
  }
415
- return (_jsxs("main", { className: "mx-auto w-full max-w-3xl space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/voorraad", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Terug naar voorraad"] }) }), !isEditingPublished && (_jsxs(Button, { variant: "outline", size: "sm", className: "ml-auto", onClick: saveDraft, disabled: submitting || !vbnValid, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), isEditingDraft ? "Concept bijwerken" : "Opslaan als concept"] })), isEditingDraft && (_jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Lokaal artikel verwijderen?" }), _jsx(AlertDialogDescription, { children: "Dit artikel is nog niet gepubliceerd naar Floriday. Verwijderen is definitief." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteDraft, children: "Verwijderen" })] })] })] }))] }), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo.url, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
722
+ 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" })), isEditingDraft ? "Concept bijwerken" : "Opslaan als concept"] })), isEditingDraft && (_jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Lokaal artikel verwijderen?" }), _jsx(AlertDialogDescription, { children: "Dit artikel is nog niet gepubliceerd naar Floriday. Verwijderen is definitief." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteDraft, children: "Verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-amber-100 text-amber-800", children: "Concept \u2014 nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsxs(_Fragment, { children: [_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: linkedProductId }), _jsx(OrderHistoryCard, { tradeItemKey: localTradeItemKey, productName: data.name || data.vbnName || null, productId: linkedProductId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: localTradeItemKey, productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
416
723
  const isOpen = i === step;
417
724
  const isValid = stepValidity[i];
418
725
  const isVisited = i < step || isEditing;
@@ -431,7 +738,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
431
738
  ? "border-primary bg-primary text-primary-foreground"
432
739
  : status === "invalid"
433
740
  ? "border-destructive bg-destructive text-destructive-foreground"
434
- : "border-muted-foreground/30 text-muted-foreground"}`, children: status === "valid" ? (_jsx(Check, { className: "h-4 w-4" })) : status === "invalid" ? ("!") : (i + 1) }), _jsx("span", { className: `min-w-0 truncate font-medium ${status === "untouched" ? "text-muted-foreground" : ""}`, children: s.label }), !isOpen && summary && (_jsx("span", { className: `max-w-xs truncate text-xs ${status === "invalid" ? "text-destructive" : "text-muted-foreground"}`, children: status === "invalid" ? `Onvolledig — ${summary}` : summary }))] }), isOpen && (_jsxs(CardContent, { className: "space-y-4 pt-0", children: [_jsx("div", { "data-wizard-form-content": true, className: "w-full min-w-0", children: _jsx(StepBody, { stepKey: s.key, data: data, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo.url, alt: "Floriday", className: "h-7 w-7 shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Label, { htmlFor: "publish-to-floriday", className: "font-medium cursor-pointer", children: "Publiceren naar Floriday" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: publishToFloriday
741
+ : "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
435
742
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
436
743
  : "Het artikel wordt alleen lokaal opgeslagen. Je kunt het later vanuit de voorraadlijst alsnog publiceren naar Floriday." })] }), _jsx(Switch, { id: "publish-to-floriday", checked: publishToFloriday, onCheckedChange: setPublishToFloriday })] })), (isLastStep || isEditing) && invalidStepCount > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive flex items-center justify-between gap-3", children: [_jsxs("span", { children: ["Nog ", invalidStepCount, " stap", invalidStepCount === 1 ? "" : "pen", " onvolledig"] }), firstInvalidIndex >= 0 && (_jsxs("button", { type: "button", onClick: () => setStep(firstInvalidIndex), className: "underline font-medium", children: ["Ga naar ", steps[firstInvalidIndex].label] }))] })), _jsx("div", { className: "flex justify-end pt-2", children: isLastStep || isEditing ? (_jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), isEditingPublished
437
744
  ? "Wijzigingen opslaan"
@@ -475,6 +782,8 @@ function summaryFor(key, d) {
475
782
  : "Alle klanten";
476
783
  case "fotos":
477
784
  return `${d.images.length} foto('s)`;
785
+ case "warehouse":
786
+ return d.warehouseId ? "Gekozen" : "Niet gekozen";
478
787
  case "beladingen":
479
788
  return d.packingConfigurations.length
480
789
  ? `${d.packingConfigurations.length} configuratie(s)`
@@ -487,6 +796,10 @@ function summaryFor(key, d) {
487
796
  return "Binnenkort beschikbaar";
488
797
  case "beschikbaarheid":
489
798
  return d.yearRound ? "Jaarrond" : `${d.weeks.length} week/weken`;
799
+ case "aanbieden":
800
+ if (!d.offerForSale)
801
+ return "Niet aanbieden";
802
+ return `${d.saleStockPieces} st • € ${d.salePricePerPiece.toFixed(2)} p/st`;
490
803
  default:
491
804
  return "";
492
805
  }
@@ -508,6 +821,8 @@ function StepBody(p) {
508
821
  return _jsx(StepKlanten, { ...p });
509
822
  case "fotos":
510
823
  return _jsx(StepFotos, { ...p });
824
+ case "warehouse":
825
+ return _jsx(StepWarehouse, { ...p });
511
826
  case "beladingen":
512
827
  return _jsx(StepBeladingen, { ...p });
513
828
  case "verpakking":
@@ -516,6 +831,8 @@ function StepBody(p) {
516
831
  return (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground", children: "Binnenkort beschikbaar" }));
517
832
  case "beschikbaarheid":
518
833
  return _jsx(StepBeschikbaarheid, { ...p });
834
+ case "aanbieden":
835
+ return _jsx(StepAanbieden, { ...p });
519
836
  }
520
837
  }
521
838
  // ============================================================
@@ -571,6 +888,9 @@ function codeOf(r) {
571
888
  // FeatureType heeft `id`.
572
889
  return String(r.data?.code ?? r.data?.id ?? r.record_id);
573
890
  }
891
+ function compareByCode(a, b) {
892
+ return String(a ?? "").localeCompare(String(b ?? ""), "nl", { numeric: true, sensitivity: "base" });
893
+ }
574
894
  // Floricode `product_group_id` is een 8-cijferige string. De eerste twee
575
895
  // cijfers bepalen het hoofdsegment, maar Floricode gebruikt méér dan één
576
896
  // bucket per Floriday-productgroep:
@@ -889,12 +1209,13 @@ function CharacteristicField({ field, value, onChange, onRemove, }) {
889
1209
  // Fallback: /gateway/code-list voor niet-VBN karakteristieken.
890
1210
  const hasSpecValues = field.allowedValues.length > 0;
891
1211
  const codeListQuery = useCodeList(hasSpecValues ? null : field.listName);
892
- const values = hasSpecValues
1212
+ const rawValues = hasSpecValues
893
1213
  ? field.allowedValues
894
1214
  : (codeListQuery.data?.values ?? []).map((v) => ({
895
1215
  code: v.code,
896
1216
  label: v.description ?? v.code,
897
1217
  }));
1218
+ const values = [...rawValues].sort((a, b) => compareByCode(a.code, b.code));
898
1219
  const CLEAR = "__clear__";
899
1220
  const invalidExistingValue = Boolean(value && values.length > 0 && !values.some((v) => v.code === value));
900
1221
  return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Label, { children: [field.label, field.required && _jsx("span", { className: "ml-1 text-destructive", children: "*" }), " ", _jsxs("span", { className: "font-mono text-xs text-muted-foreground", children: ["(", field.code, ")"] })] }), onRemove && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0 text-muted-foreground hover:text-destructive", onClick: onRemove, "aria-label": `Verwijder ${field.label}`, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }), !hasSpecValues && codeListQuery.isLoading ? (_jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), "Waardelijst ", field.listName, " wordt geladen\u2026"] })) : values.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Select, { value: value || undefined, onValueChange: (v) => onChange(v === CLEAR ? "" : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer een door Floriday toegestane waarde\u2026" }) }), _jsxs(SelectContent, { children: [value && (_jsx(SelectItem, { value: CLEAR, children: _jsx("span", { className: "text-muted-foreground", children: "\u2014 Wissen \u2014" }) })), values.slice(0, 1000).map((v) => (_jsxs(SelectItem, { value: v.code, children: [v.label, " ", _jsxs("span", { className: "font-mono text-xs text-muted-foreground", children: ["(", v.code, ")"] })] }, v.code)))] })] }), invalidExistingValue && (_jsxs("p", { className: "text-xs text-destructive", children: ["Huidige waarde \"", value, "\" komt niet voor in de door Floriday toegestane lijst. Kies een geldige waarde."] }))] })) : (_jsx("p", { className: "rounded-md border border-dashed border-muted-foreground/30 bg-muted/40 p-2 text-xs text-muted-foreground", children: "Geen toegestane waarden bekend voor deze karakteristiek. Laat leeg \u2014 vrije invoer wordt door Floriday niet geaccepteerd." }))] }));
@@ -1028,6 +1349,49 @@ function StepFotos({ data, update, activeId }) {
1028
1349
  e.target.value = "";
1029
1350
  } })] }), data.images.length > 0 && (_jsx("div", { className: "grid grid-cols-4 gap-2", children: data.images.map((img, idx) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border", children: [_jsx("img", { src: img.previewUrl || img.url, alt: img.name, className: "h-24 w-full object-cover" }), _jsx("button", { type: "button", onClick: () => update("images", data.images.filter((_, i) => i !== idx)), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1 opacity-0 group-hover:opacity-100", children: _jsx(X, { className: "h-3 w-3" }) })] }, img.url))) }))] }));
1030
1351
  }
1352
+ /**
1353
+ * Warehouse-stap — verplicht in Floriday Suppliers API 2026v1.
1354
+ * Toont een select gevuld uit `floriday_warehouses_cache`. Beheer van
1355
+ * warehouses (aanmaken/bewerken) gebeurt in Instellingen → Warehouses.
1356
+ */
1357
+ function StepWarehouse({ data, update, activeId }) {
1358
+ const [warehouses, setWarehouses] = useState([]);
1359
+ const [loading, setLoading] = useState(false);
1360
+ useEffect(() => {
1361
+ if (!activeId)
1362
+ return;
1363
+ let cancelled = false;
1364
+ setLoading(true);
1365
+ supabase
1366
+ .from("floriday_warehouses_cache")
1367
+ .select("floriday_id, data")
1368
+ .eq("connection_id", activeId)
1369
+ .then(({ data: rows }) => {
1370
+ if (cancelled)
1371
+ return;
1372
+ const items = (rows ?? [])
1373
+ // Alleen eigen voorraadlocaties tonen — geen externe FloraHolland-
1374
+ // aflever-/veilinglocaties (isExternal=true).
1375
+ .filter((r) => r.data?.isExternal !== true && r.data?.isDeleted !== true)
1376
+ .map((r) => {
1377
+ const label = warehouseDisplayLabel(r.data, String(r.floriday_id));
1378
+ const sub = warehouseSubLabel(r.data);
1379
+ return {
1380
+ id: String(r.floriday_id),
1381
+ name: label,
1382
+ sub: label === sub ? undefined : sub,
1383
+ };
1384
+ })
1385
+ .sort((a, b) => a.name.localeCompare(b.name));
1386
+ setWarehouses(items);
1387
+ setLoading(false);
1388
+ });
1389
+ return () => {
1390
+ cancelled = true;
1391
+ };
1392
+ }, [activeId]);
1393
+ return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Voorraadlocatie (verplicht)" }), _jsxs(Select, { value: data.warehouseId || undefined, onValueChange: (v) => update("warehouseId", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? "Laden…" : "Kies een voorraadlocatie" }) }), _jsx(SelectContent, { children: warehouses.map((w) => (_jsx(SelectItem, { value: w.id, children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { children: w.name }), w.sub && (_jsx("span", { className: "text-xs text-muted-foreground", children: w.sub }))] }) }, w.id))) })] })] }), warehouses.length === 0 && !loading && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Nog geen eigen voorraadlocaties gevonden. Ga naar", " ", _jsx(Link, { to: "/instellingen/warehouses", className: "underline", children: "Instellingen \u2192 Warehouses" }), " ", "om er \u00E9\u00E9n aan te maken."] }))] }));
1394
+ }
1031
1395
  /**
1032
1396
  * Beladingen — Floriday Suppliers API 2026v1
1033
1397
  *
@@ -1085,7 +1449,9 @@ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCu
1085
1449
  return (_jsxs("div", { className: "space-y-4", children: [data.packingConfigurations.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen beladingsconfiguratie toegevoegd." }) })) : (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 font-medium", children: "Primair" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Fust" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Ladingdrager" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Stuks / laag / drager" }), _jsx("th", { className: "px-3 py-2 font-medium", children: "Type" }), _jsx("th", { className: "px-3 py-2" })] }) }), _jsx("tbody", { className: "divide-y", children: data.packingConfigurations.map((p) => {
1086
1450
  const pkgLabel = packagingTypes.find((r) => codeOf(r) === p.packagingType);
1087
1451
  const lcLabel = loadCarriers.find((r) => codeOf(r) === p.loadCarrier);
1088
- return (_jsxs("tr", { className: "hover:bg-muted/30", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "radio", name: "primary-packing", checked: Boolean(p.isPrimary), onChange: () => setPrimary(p.id), "aria-label": "Als primaire belading instellen" }) }), _jsxs("td", { className: "px-3 py-2", children: [_jsx("div", { className: "font-medium", children: pkgLabel ? labelOf(pkgLabel) : p.packagingType || "—" }), p.source === "SUPPLIER" && (_jsx("div", { className: "text-xs text-muted-foreground", children: p.packagingSupplierName ?? p.packagingSupplierId }))] }), _jsx("td", { className: "px-3 py-2", children: lcLabel ? labelOf(lcLabel) : p.loadCarrier || "—" }), _jsxs("td", { className: "px-3 py-2 tabular-nums", children: [p.unitsPerPackage, " / ", p.packagesPerLayer, " /", " ", p.layersPerLoadCarrier] }), _jsx("td", { className: "px-3 py-2", children: p.isCustomerSpecific ? (_jsxs("span", { className: "rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-900", children: ["Klantspecifiek (", p.customerOrganizationIds.length, ")"] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "Regulier" })) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => openEdit(p.id), children: "Bewerk" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, p.id));
1452
+ 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
1453
+ ? `${codeOf(pkgLabel)}: ${labelOf(pkgLabel)}`
1454
+ : 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));
1089
1455
  }) })] }) })), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: openNew, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Belading toevoegen"] }), _jsx(PackingConfigDialog, { open: dialogOpen, onOpenChange: (o) => {
1090
1456
  setDialogOpen(o);
1091
1457
  if (!o)
@@ -1099,7 +1465,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1099
1465
  const emptyItem = () => ({
1100
1466
  id: crypto.randomUUID(),
1101
1467
  source: "VBN",
1102
- loadCarrier: "",
1468
+ loadCarrier: "AUCTION_TROLLEY",
1103
1469
  packagingType: "",
1104
1470
  unitsPerPackage: 1,
1105
1471
  packagesPerLayer: 1,
@@ -1122,9 +1488,10 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1122
1488
  const patch = (k, v) => setDraft((d) => ({ ...d, [k]: v }));
1123
1489
  const filteredPkg = useMemo(() => {
1124
1490
  const q = pkgSearch.trim().toLowerCase();
1491
+ const sorted = [...packagingTypes].sort((a, b) => compareByCode(codeOf(a), codeOf(b)));
1125
1492
  if (!q)
1126
- return packagingTypes.slice(0, 200);
1127
- return packagingTypes
1493
+ return sorted.slice(0, 200);
1494
+ return sorted
1128
1495
  .filter((r) => labelOf(r).toLowerCase().includes(q) ||
1129
1496
  codeOf(r).toLowerCase().includes(q))
1130
1497
  .slice(0, 200);
@@ -1184,7 +1551,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1184
1551
  ? (packagingTypes.find((r) => codeOf(r) === draft.packagingType)
1185
1552
  ? `${labelOf(packagingTypes.find((r) => codeOf(r) === draft.packagingType))} (${draft.packagingType})`
1186
1553
  : draft.packagingType)
1187
- : "Selecteer fust..." }), _jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[380px] p-0", align: "start", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { placeholder: "Zoek op naam of code...", value: pkgSearch, onChange: (e) => setPkgSearch(e.target.value) }) }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredPkg.length === 0 ? (_jsx("div", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: "Geen resultaten" })) : (filteredPkg.map((r) => (_jsxs("button", { type: "button", onClick: () => patch("packagingType", codeOf(r)), className: `flex w-full items-start gap-2 px-3 py-2 text-left text-sm hover:bg-muted ${draft.packagingType === codeOf(r) ? "bg-muted" : ""}`, children: [_jsx("span", { className: "font-mono text-xs text-muted-foreground w-14 shrink-0", children: codeOf(r) }), _jsx("span", { className: "truncate", children: labelOf(r) })] }, r.record_id)))) })] })] })] }), _jsx(DropdownOrInput, { label: "Ladingdrager", value: draft.loadCarrier, onChange: (v) => patch("loadCarrier", v), records: loadCarriers, render: (r) => `${labelOf(r)} (${codeOf(r)})`, valueGetter: codeOf, emptyHint: "Geen LoadCarrier-records gesynchroniseerd." })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Stuks per fust" }), _jsx(Input, { type: "number", min: 1, value: draft.unitsPerPackage, onChange: (e) => patch("unitsPerPackage", Number(e.target.value) || 1) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Fusten per laag" }), _jsx(Input, { type: "number", min: 1, value: draft.packagesPerLayer, onChange: (e) => patch("packagesPerLayer", Number(e.target.value) || 1) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Lagen per drager" }), _jsx(Input, { type: "number", min: 1, value: draft.layersPerLoadCarrier, onChange: (e) => patch("layersPerLoadCarrier", Number(e.target.value) || 1) })] })] }), _jsxs("div", { className: "rounded-md border p-3 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Klantspecifieke belading" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Alleen zichtbaar voor geselecteerde klantorganisaties." })] }), _jsx(Switch, { checked: draft.isCustomerSpecific, onCheckedChange: (v) => patch("isCustomerSpecific", Boolean(v)) })] }), draft.isCustomerSpecific && (_jsx("div", { className: "max-h-40 overflow-y-auto rounded border divide-y", children: floridayCustomers.length === 0 ? (_jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "Geen klantorganisaties geladen." })) : (floridayCustomers.map((c) => (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.customerOrganizationIds.includes(c.id), onChange: () => toggleCustomer(c.id) }), _jsx("span", { className: "truncate", children: c.name })] }, c.id)))) }))] }), _jsxs("div", { className: "rounded-md border p-3 space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Foto van dit fust (optioneel)" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "JPG of PNG \u00B7 min. 640 \u00D7 640 px \u00B7 max. 5 MB" })] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: uploadingPhoto || !activeId, onClick: () => photoInputRef.current?.click(), children: uploadingPhoto ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), " Uploaden"] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), " Foto kiezen"] })) }), _jsx("input", { ref: photoInputRef, type: "file", accept: "image/png,image/jpeg", className: "hidden", onChange: (e) => {
1554
+ : "Selecteer fust..." }), _jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[380px] p-0", align: "start", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { placeholder: "Zoek op naam of code...", value: pkgSearch, onChange: (e) => setPkgSearch(e.target.value) }) }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredPkg.length === 0 ? (_jsx("div", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: "Geen resultaten" })) : (filteredPkg.map((r) => (_jsxs("button", { type: "button", onClick: () => patch("packagingType", codeOf(r)), className: `flex w-full items-start gap-2 px-3 py-2 text-left text-sm hover:bg-muted ${draft.packagingType === codeOf(r) ? "bg-muted" : ""}`, children: [_jsx("span", { className: "font-mono text-xs text-muted-foreground w-14 shrink-0", children: codeOf(r) }), _jsx("span", { className: "truncate", children: labelOf(r) })] }, r.record_id)))) })] })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Ladingdrager" }), _jsxs(Select, { value: draft.loadCarrier || "AUCTION_TROLLEY", onValueChange: (v) => patch("loadCarrier", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een ladingdrager" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "AUCTION_TROLLEY", children: "Stapelwagen" }), _jsx(SelectItem, { value: "DANISH_TROLLEY", children: "Deense container" }), _jsx(SelectItem, { value: "EURO_TROLLEY", children: "Euro-rolcontainer" }), _jsx(SelectItem, { value: "PALLET", children: "Pallet" }), _jsx(SelectItem, { value: "EURO_PALLET", children: "Euro-pallet" }), _jsx(SelectItem, { value: "NONE", children: "Geen ladingdrager" })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Stuks per fust" }), _jsx(Input, { type: "number", min: 1, value: draft.unitsPerPackage, onChange: (e) => patch("unitsPerPackage", Number(e.target.value) || 1) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Fusten per laag" }), _jsx(Input, { type: "number", min: 1, value: draft.packagesPerLayer, onChange: (e) => patch("packagesPerLayer", Number(e.target.value) || 1) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Lagen per drager" }), _jsx(Input, { type: "number", min: 1, value: draft.layersPerLoadCarrier, onChange: (e) => patch("layersPerLoadCarrier", Number(e.target.value) || 1) })] })] }), _jsxs("div", { className: "rounded-md border p-3 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Klantspecifieke belading" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Alleen zichtbaar voor geselecteerde klantorganisaties." })] }), _jsx(Switch, { checked: draft.isCustomerSpecific, onCheckedChange: (v) => patch("isCustomerSpecific", Boolean(v)) })] }), draft.isCustomerSpecific && (_jsx("div", { className: "max-h-40 overflow-y-auto rounded border divide-y", children: floridayCustomers.length === 0 ? (_jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "Geen klantorganisaties geladen." })) : (floridayCustomers.map((c) => (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.customerOrganizationIds.includes(c.id), onChange: () => toggleCustomer(c.id) }), _jsx("span", { className: "truncate", children: c.name })] }, c.id)))) }))] }), _jsxs("div", { className: "rounded-md border p-3 space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Foto van dit fust (optioneel)" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "JPG of PNG \u00B7 min. 640 \u00D7 640 px \u00B7 max. 5 MB" })] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: uploadingPhoto || !activeId, onClick: () => photoInputRef.current?.click(), children: uploadingPhoto ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), " Uploaden"] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), " Foto kiezen"] })) }), _jsx("input", { ref: photoInputRef, type: "file", accept: "image/png,image/jpeg", className: "hidden", onChange: (e) => {
1188
1555
  onUploadPhoto(e.target.files?.[0]);
1189
1556
  e.target.value = "";
1190
1557
  } })] }), (draft.photoPreviewUrl || draft.photoUrl) && (_jsxs("div", { className: "relative inline-block", children: [_jsx("img", { src: draft.photoPreviewUrl || draft.photoUrl, alt: "Belading foto", className: "h-24 rounded-md border object-cover" }), _jsx("button", { type: "button", onClick: () => setDraft((d) => ({
@@ -1195,13 +1562,10 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
1195
1562
  })), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1", children: _jsx(X, { className: "h-3 w-3" }) })] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: Boolean(draft.isPrimary), onChange: (e) => patch("isPrimary", e.target.checked) }), "Als primaire belading gebruiken"] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { type: "button", disabled: !canSave, onClick: () => onSave(draft), children: "Opslaan" })] })] }) }));
1196
1563
  }
1197
1564
  function StepVerpakking({ data, update }) {
1198
- const add = () => update("additionalPackaging", [
1199
- ...data.additionalPackaging,
1200
- { id: crypto.randomUUID(), code: "", description: "", quantity: 1 },
1201
- ]);
1565
+ const [dialogOpen, setDialogOpen] = useState(false);
1566
+ const addItem = (item) => update("additionalPackaging", [...data.additionalPackaging, item]);
1202
1567
  const remove = (id) => update("additionalPackaging", data.additionalPackaging.filter((p) => p.id !== id));
1203
- const edit = (id, patch) => update("additionalPackaging", data.additionalPackaging.map((p) => (p.id === id ? { ...p, ...patch } : p)));
1204
- return (_jsxs("div", { className: "space-y-3", children: [data.additionalPackaging.map((p) => (_jsxs("div", { className: "grid grid-cols-[1fr_2fr_100px_auto] gap-2 items-end", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Code" }), _jsx(Input, { value: p.code, onChange: (e) => edit(p.id, { code: e.target.value }), placeholder: "bv. HOES" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Omschrijving" }), _jsx(Input, { value: p.description ?? "", onChange: (e) => edit(p.id, { description: e.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Aantal" }), _jsx(Input, { type: "number", min: 1, value: p.quantity, onChange: (e) => edit(p.id, { quantity: Number(e.target.value) }) })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: add, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Verpakkingsmateriaal toevoegen"] })] }));
1568
+ 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 })] }));
1205
1569
  }
1206
1570
  function StepBeschikbaarheid({ data, update }) {
1207
1571
  const weeks = Array.from({ length: 52 }, (_, i) => i + 1);
@@ -1217,6 +1581,22 @@ function StepBeschikbaarheid({ data, update }) {
1217
1581
  ? "border-primary bg-primary text-primary-foreground"
1218
1582
  : "hover:border-primary/40"}`, children: w }, w))) })] }))] }));
1219
1583
  }
1584
+ // ------------------------------------------------------------
1585
+ // Stap "Aanbieden & voorraad" — Direct Sale via CustomerOffer
1586
+ // ------------------------------------------------------------
1587
+ function StepAanbieden({ data, update }) {
1588
+ const priceStr = Number.isFinite(data.salePricePerPiece)
1589
+ ? String(data.salePricePerPiece)
1590
+ : "";
1591
+ const stockStr = Number.isFinite(data.saleStockPieces)
1592
+ ? String(data.saleStockPieces)
1593
+ : "";
1594
+ 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
1595
+ ? "Aanbod wordt aangemaakt direct na publiceren."
1596
+ : "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
1597
+ ? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
1598
+ : "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }))] }));
1599
+ }
1220
1600
  // ============================================================
1221
1601
  // Shared
1222
1602
  // ============================================================
@@ -1225,5 +1605,8 @@ function DropdownOrInput({ label, value, onChange, records, render, valueGetter,
1225
1605
  if (records.length === 0) {
1226
1606
  return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsx(Input, { value: value, onChange: (e) => onChange(e.target.value) }), emptyHint && _jsx("p", { className: "text-xs text-muted-foreground", children: emptyHint })] }));
1227
1607
  }
1228
- return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsxs(Select, { value: value || undefined, onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer..." }) }), _jsx(SelectContent, { children: records.slice(0, 300).map((r) => (_jsx(SelectItem, { value: v(r), children: render(r) }, r.record_id))) })] })] }));
1608
+ return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsxs(Select, { value: value || undefined, onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer..." }) }), _jsx(SelectContent, { children: [...records]
1609
+ .sort((a, b) => compareByCode(v(a), v(b)))
1610
+ .slice(0, 300)
1611
+ .map((r) => (_jsx(SelectItem, { value: v(r), children: render(r) }, r.record_id))) })] })] }));
1229
1612
  }