@flowselections/floriday-voorraad 1.0.27 → 1.0.29

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 (127) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts +2 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  3. package/dist-lib/components/voorraad/ArtikelWizard.js +576 -324
  4. package/dist-lib/components/voorraad/EditArtikelPage.d.ts +2 -1
  5. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
  6. package/dist-lib/components/voorraad/EditArtikelPage.js +5 -4
  7. package/dist-lib/components/voorraad/FustenPage.d.ts +2 -0
  8. package/dist-lib/components/voorraad/FustenPage.d.ts.map +1 -0
  9. package/dist-lib/components/voorraad/FustenPage.js +97 -0
  10. package/dist-lib/components/voorraad/LogistiekTakenCard.d.ts +2 -0
  11. package/dist-lib/components/voorraad/LogistiekTakenCard.d.ts.map +1 -0
  12. package/dist-lib/components/voorraad/LogistiekTakenCard.js +54 -0
  13. package/dist-lib/components/voorraad/ProductCustomFieldsCard.d.ts +5 -0
  14. package/dist-lib/components/voorraad/ProductCustomFieldsCard.d.ts.map +1 -0
  15. package/dist-lib/components/voorraad/ProductCustomFieldsCard.js +65 -0
  16. package/dist-lib/components/voorraad/PublicatiesPage.d.ts +2 -0
  17. package/dist-lib/components/voorraad/PublicatiesPage.d.ts.map +1 -0
  18. package/dist-lib/components/voorraad/PublicatiesPage.js +60 -0
  19. package/dist-lib/components/voorraad/PublishProgressCard.d.ts +12 -0
  20. package/dist-lib/components/voorraad/PublishProgressCard.d.ts.map +1 -0
  21. package/dist-lib/components/voorraad/PublishProgressCard.js +30 -0
  22. package/dist-lib/components/voorraad/PublishReadinessCard.d.ts +10 -0
  23. package/dist-lib/components/voorraad/PublishReadinessCard.d.ts.map +1 -0
  24. package/dist-lib/components/voorraad/PublishReadinessCard.js +12 -0
  25. package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -1
  26. package/dist-lib/components/voorraad/StockBulkButtons.js +65 -2
  27. package/dist-lib/components/voorraad/StockSyncStatus.d.ts +2 -0
  28. package/dist-lib/components/voorraad/StockSyncStatus.d.ts.map +1 -0
  29. package/dist-lib/components/voorraad/StockSyncStatus.js +23 -0
  30. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
  31. package/dist-lib/components/voorraad/ValidationOverviewTable.js +8 -2
  32. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts.map +1 -1
  33. package/dist-lib/components/voorraad/VoorraadDashboard.js +4 -1
  34. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  35. package/dist-lib/components/voorraad/VoorraadPage.js +5 -1
  36. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  37. package/dist-lib/components/voorraad/VoorraadTable.js +10 -2
  38. package/dist-lib/components/voorraad/VoorraadVeldenCard.d.ts +2 -0
  39. package/dist-lib/components/voorraad/VoorraadVeldenCard.d.ts.map +1 -0
  40. package/dist-lib/components/voorraad/VoorraadVeldenCard.js +97 -0
  41. package/dist-lib/hooks/useFusten.d.ts +17 -0
  42. package/dist-lib/hooks/useFusten.d.ts.map +1 -0
  43. package/dist-lib/hooks/useFusten.js +81 -0
  44. package/dist-lib/hooks/useLocalTradeItems.d.ts +17 -7
  45. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  46. package/dist-lib/hooks/useLocalTradeItems.js +139 -37
  47. package/dist-lib/hooks/useLogisticsTasks.d.ts +13 -0
  48. package/dist-lib/hooks/useLogisticsTasks.d.ts.map +1 -0
  49. package/dist-lib/hooks/useLogisticsTasks.js +155 -0
  50. package/dist-lib/hooks/usePublishJob.d.ts +13 -0
  51. package/dist-lib/hooks/usePublishJob.d.ts.map +1 -0
  52. package/dist-lib/hooks/usePublishJob.js +67 -0
  53. package/dist-lib/hooks/useRegisterModuleBaseUrl.d.ts +2 -0
  54. package/dist-lib/hooks/useRegisterModuleBaseUrl.d.ts.map +1 -0
  55. package/dist-lib/hooks/useRegisterModuleBaseUrl.js +31 -0
  56. package/dist-lib/hooks/useStockSyncHealth.d.ts +8 -0
  57. package/dist-lib/hooks/useStockSyncHealth.d.ts.map +1 -0
  58. package/dist-lib/hooks/useStockSyncHealth.js +42 -0
  59. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
  60. package/dist-lib/hooks/useTradeItemStock.js +37 -1
  61. package/dist-lib/hooks/useVoorraadCustomFields.d.ts +9 -0
  62. package/dist-lib/hooks/useVoorraadCustomFields.d.ts.map +1 -0
  63. package/dist-lib/hooks/useVoorraadCustomFields.js +26 -0
  64. package/dist-lib/index.d.ts +2 -0
  65. package/dist-lib/index.d.ts.map +1 -1
  66. package/dist-lib/index.js +9 -2
  67. package/dist-lib/integrations/supabase/auth-middleware.d.ts +8467 -4487
  68. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  69. package/dist-lib/integrations/supabase/client.d.ts +8467 -4487
  70. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  71. package/dist-lib/integrations/supabase/client.server.d.ts +8467 -4487
  72. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  73. package/dist-lib/integrations/supabase/types.d.ts +8714 -4631
  74. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  75. package/dist-lib/integrations/supabase/types.js +49 -0
  76. package/dist-lib/lib/floricode-required-features.functions.d.ts +8467 -4487
  77. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  78. package/dist-lib/lib/floriday-client.d.ts +1 -1
  79. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  80. package/dist-lib/lib/floriday-client.js +1 -2
  81. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +8467 -4487
  82. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  83. package/dist-lib/lib/floriday-customer-offer.functions.js +14 -19
  84. package/dist-lib/lib/floriday-env.functions.d.ts +35712 -0
  85. package/dist-lib/lib/floriday-env.functions.d.ts.map +1 -0
  86. package/dist-lib/lib/floriday-env.functions.js +16 -0
  87. package/dist-lib/lib/floriday-env.server.d.ts +12 -0
  88. package/dist-lib/lib/floriday-env.server.d.ts.map +1 -1
  89. package/dist-lib/lib/floriday-env.server.js +15 -2
  90. package/dist-lib/lib/floriday-gateway.functions.d.ts +77902 -40145
  91. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  92. package/dist-lib/lib/floriday-gateway.functions.js +12 -3
  93. package/dist-lib/lib/floriday-readiness.server.d.ts +11 -0
  94. package/dist-lib/lib/floriday-readiness.server.d.ts.map +1 -0
  95. package/dist-lib/lib/floriday-readiness.server.js +43 -0
  96. package/dist-lib/lib/floriday-vbn-required.functions.d.ts +23115 -15155
  97. package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -1
  98. package/dist-lib/lib/floriday-warehouse.functions.d.ts +31362 -19422
  99. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  100. package/dist-lib/lib/floriday-warehouse.functions.js +3 -0
  101. package/dist-lib/lib/floriday-writes.functions.d.ts +23191 -15231
  102. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  103. package/dist-lib/lib/fusten.d.ts +87 -0
  104. package/dist-lib/lib/fusten.d.ts.map +1 -0
  105. package/dist-lib/lib/fusten.js +389 -0
  106. package/dist-lib/lib/logistics-engine.d.ts +63 -0
  107. package/dist-lib/lib/logistics-engine.d.ts.map +1 -0
  108. package/dist-lib/lib/logistics-engine.js +57 -0
  109. package/dist-lib/lib/logistics-events.d.ts +18 -0
  110. package/dist-lib/lib/logistics-events.d.ts.map +1 -0
  111. package/dist-lib/lib/logistics-events.js +61 -0
  112. package/dist-lib/lib/module-runtime-config.functions.d.ts +14 -0
  113. package/dist-lib/lib/module-runtime-config.functions.d.ts.map +1 -0
  114. package/dist-lib/lib/module-runtime-config.functions.js +107 -0
  115. package/dist-lib/lib/publish-errors.d.ts +37 -0
  116. package/dist-lib/lib/publish-errors.d.ts.map +1 -0
  117. package/dist-lib/lib/publish-errors.js +183 -0
  118. package/dist-lib/lib/publish-pipeline.d.ts +91 -0
  119. package/dist-lib/lib/publish-pipeline.d.ts.map +1 -0
  120. package/dist-lib/lib/publish-pipeline.js +212 -0
  121. package/dist-lib/lib/publish-readiness.d.ts +42 -0
  122. package/dist-lib/lib/publish-readiness.d.ts.map +1 -0
  123. package/dist-lib/lib/publish-readiness.js +102 -0
  124. package/dist-lib/lib/voorraad-custom-fields.d.ts +22 -0
  125. package/dist-lib/lib/voorraad-custom-fields.d.ts.map +1 -0
  126. package/dist-lib/lib/voorraad-custom-fields.js +71 -0
  127. package/package.json +2 -2
@@ -12,16 +12,23 @@ import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, buildPrimaryPack
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
- import { deleteLocalTradeItem, getLocalTradeItem, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
15
+ import { deleteLocalTradeItem, getLocalTradeItem, markLocalSyncStatus, readLocalSyncStatus, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
16
16
  import { useTradeItemSpec } from "../../hooks/useTradeItemSpec";
17
- import { isMissingCharacteristicsError } from "../../lib/floriday-writes.functions";
18
17
  import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures";
19
18
  import { useFloridayVbnRequired } from "../../hooks/useFloridayVbnRequired";
20
19
  import { saveFloridayVbnRequired } from "../../lib/floriday-vbn-required.functions";
21
- import { useQueryClient } from "@tanstack/react-query";
20
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
22
21
  import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday";
23
22
  import { useFloridayPriceGroups } from "../../hooks/useFloridayPriceGroups";
24
23
  import { readTradeItemPrices, syncTradeItemPrices } from "../../lib/trade-item-prices";
24
+ import { createPublishJob, finishPublishJob, markStepSkipped, rememberPublishRejection, runPublishStep, } from "../../lib/publish-pipeline";
25
+ import { translatePublishError, } from "../../lib/publish-errors";
26
+ import { buildReadinessReport } from "../../lib/publish-readiness";
27
+ import { usePublishJob } from "../../hooks/usePublishJob";
28
+ import { PublishReadinessCard } from "./PublishReadinessCard";
29
+ import { PublishProgressCard } from "./PublishProgressCard";
30
+ import { gatewayReadFn } from "../../lib/floriday-gateway.functions";
31
+ import { getFloridayReadinessFn } from "../../lib/floriday-env.functions";
25
32
  const floridayLogo = "/flowselections-assets/Voorraad/floriday-logo.png";
26
33
  import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
27
34
  import { ProductLinkCard } from "../../components/voorraad/ProductLinkCard";
@@ -78,6 +85,11 @@ function warehouseSubLabel(raw) {
78
85
  const city = addr.city ?? "";
79
86
  return [line, city].filter(Boolean).join(", ") || undefined;
80
87
  }
88
+ function asPlainRecord(value) {
89
+ return value && typeof value === "object" && !Array.isArray(value)
90
+ ? value
91
+ : {};
92
+ }
81
93
  async function lookupProductGroupForVbn(vbnCode) {
82
94
  const code = String(vbnCode ?? "").trim();
83
95
  if (!code)
@@ -90,14 +102,23 @@ async function lookupProductGroupForVbn(vbnCode) {
90
102
  .maybeSingle();
91
103
  return data ? groupOfRecord(data) : null;
92
104
  }
93
- export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, } = {}) {
105
+ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, resumeJobId, } = {}) {
94
106
  const navigate = useNavigate();
95
107
  const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
108
+ const connectionReadiness = useQuery({
109
+ queryKey: ["floriday-readiness", activeId, activeConnection?.active_environment],
110
+ queryFn: () => getFloridayReadinessFn(),
111
+ enabled: Boolean(activeId),
112
+ staleTime: 30000,
113
+ retry: false,
114
+ });
96
115
  const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
97
116
  const { priceGroups } = useFloridayPriceGroups(activeId);
98
117
  const [step, setStep] = useState(0);
99
118
  const [data, setData] = useState(empty);
100
119
  const [submitting, setSubmitting] = useState(false);
120
+ const [publishJobId, setPublishJobId] = useState(resumeJobId ?? null);
121
+ const { job: publishJob, steps: publishSteps } = usePublishJob(publishJobId);
101
122
  const [publishToFloriday, setPublishToFloriday] = useState(true);
102
123
  const [draftId, setDraftId] = useState(initialDraftId);
103
124
  const [draftFloridayId, setDraftFloridayId] = useState(null);
@@ -106,6 +127,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
106
127
  const [productLoading, setProductLoading] = useState(Boolean(productId));
107
128
  const [syncState, setSyncState] = useState("idle");
108
129
  const [syncError, setSyncError] = useState(null);
130
+ // Lokaal opgeslagen, maar (nog) niet bevestigd door Floriday.
131
+ const [pendingSync, setPendingSync] = useState(null);
132
+ const [retryingSync, setRetryingSync] = useState(false);
109
133
  const [productMeta, setProductMeta] = useState({ floridayActive: false, floridayTradeItemId: null, physicalStock: 0 });
110
134
  const [deleteAck, setDeleteAck] = useState(false);
111
135
  const [deleteOpen, setDeleteOpen] = useState(false);
@@ -257,7 +281,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
257
281
  try {
258
282
  const { data: product, error } = await supabase
259
283
  .from("products")
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")
284
+ .select("id,product,barcode,article_number,quantity,image_url,plants_per_crate,crates_per_layer,layers_per_cart,trade_item_key,floriday_trade_item_id,custom_fields")
261
285
  .eq("id", productId)
262
286
  .maybeSingle();
263
287
  if (cancelled)
@@ -274,9 +298,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
274
298
  const ppc = Number(product.plants_per_crate) || 0;
275
299
  const cpl = Number(product.crates_per_layer) || 0;
276
300
  const lpc = Number(product.layers_per_cart) || 0;
277
- const payload = (product.floriday_payload ?? {});
278
- const fid = (product.floriday_trade_item_id ?? null);
279
- const fActive = Boolean(product.floriday_active);
301
+ const customFields = asPlainRecord(product.custom_fields);
302
+ const payload = asPlainRecord(customFields.floriday_payload);
303
+ const productTradeItemKey = String(product.trade_item_key ?? "");
304
+ const fid = ((product.floriday_trade_item_id ??
305
+ customFields.floriday_id ??
306
+ (productTradeItemKey && !productTradeItemKey.startsWith("product:")
307
+ ? productTradeItemKey
308
+ : null)) || null);
309
+ const fActive = Boolean(fid);
280
310
  // Floriday-cache + warehouse-voorkeur parallel ophalen wanneer we een
281
311
  // gekoppeld trade-item hebben. Missing cache blokkeert de wizard niet;
282
312
  // dan valt hij terug op wat er lokaal is opgeslagen.
@@ -313,6 +343,20 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
313
343
  console.warn("Floriday-cache laden mislukt", e);
314
344
  }
315
345
  }
346
+ // Lokale wijzigingen die nog niet door Floriday zijn bevestigd zijn
347
+ // leidend: anders zou de (verouderde) Floriday-kopie de invoer van de
348
+ // gebruiker overschrijven en lijken beladingen/foto's "verwijderd".
349
+ const localSync = readLocalSyncStatus(customFields);
350
+ if (localSync?.dirty) {
351
+ cacheData = null;
352
+ setPendingSync({
353
+ error: localSync.last_error ?? null,
354
+ at: localSync.last_attempt_at ?? null,
355
+ });
356
+ }
357
+ else {
358
+ setPendingSync(null);
359
+ }
316
360
  const cachePackings = cacheData?.packingConfigurations ?? [];
317
361
  const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
318
362
  const seededPacking = cachePackings.length > 0
@@ -402,17 +446,19 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
402
446
  : validGtin
403
447
  ? barcode
404
448
  : (payload.gtin ?? ""),
405
- salePricePerPiece: Number(product.sale_price ?? payload.salePricePerPiece) || 0,
449
+ salePricePerPiece: Number(customFields.sale_price ?? customFields.salePrice ?? payload.salePricePerPiece) || 0,
406
450
  // Voorkeur: werkelijke voorraad uit trade_item_stock.
407
451
  // Fallback: opgeslagen payload-waarde. `products.quantity` wordt
408
452
  // bewust NIET meer gelezen — die staat structureel op 0.
409
453
  saleStockPieces: physical > 0
410
454
  ? physical
411
455
  : Number(payload.saleStockPieces ?? 0) || 0,
412
- vatRate: product.vat_rate !== null && product.vat_rate !== undefined
413
- ? Number(product.vat_rate)
456
+ vatRate: customFields.vat_rate !== null && customFields.vat_rate !== undefined
457
+ ? Number(customFields.vat_rate)
414
458
  : (payload.vatRate ?? null),
415
- vatCode: product.vat_code ?? payload.vatCode ?? null,
459
+ vatCode: (customFields.vat_code ?? customFields.vatCode) ??
460
+ payload.vatCode ??
461
+ null,
416
462
  warehouseId: warehousePref ||
417
463
  cacheWarehouseId ||
418
464
  payloadWarehouseId ||
@@ -473,14 +519,45 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
473
519
  toast.error("Artikel niet gevonden in de cache");
474
520
  return;
475
521
  }
522
+ // Lokale rij met nog niet doorgegeven wijzigingen? Die is leidend.
523
+ let localRow = null;
524
+ try {
525
+ const { data: prod } = await supabase
526
+ .from("products")
527
+ .select("id,custom_fields")
528
+ .eq("floriday_trade_item_id", tradeItemId)
529
+ .maybeSingle();
530
+ localRow = prod ?? null;
531
+ }
532
+ catch {
533
+ /* geen lokale rij; cache is dan leidend */
534
+ }
535
+ if (cancelled)
536
+ return;
537
+ const localSync = readLocalSyncStatus(localRow?.custom_fields);
538
+ const localPayload = localRow?.custom_fields?.floriday_payload;
539
+ if (localRow?.id)
540
+ setDraftId(localRow.id);
541
+ if (localSync?.dirty) {
542
+ setPendingSync({
543
+ error: localSync.last_error ?? null,
544
+ at: localSync.last_attempt_at ?? null,
545
+ });
546
+ }
547
+ else {
548
+ setPendingSync(null);
549
+ }
476
550
  const wizardData = floridayCacheToWizardData(row.data);
477
- const productGroup = wizardData.productGroup ??
478
- (await lookupProductGroupForVbn(wizardData.vbnCode));
551
+ const baseData = localSync?.dirty && localPayload
552
+ ? { ...wizardData, ...localPayload }
553
+ : wizardData;
554
+ const productGroup = baseData.productGroup ??
555
+ (await lookupProductGroupForVbn(baseData.vbnCode));
479
556
  const prefWarehouseId = pref?.warehouse_id ?? "";
480
- const cachedWarehouseId = wizardData.warehouseId ?? "";
557
+ const cachedWarehouseId = baseData.warehouseId ?? "";
481
558
  const preferredWarehouseFallback = prefWarehouseId || cachedWarehouseId ? null : await resolvePreferredWarehouseId();
482
559
  setData({
483
- ...wizardData,
560
+ ...baseData,
484
561
  productGroup,
485
562
  warehouseId: prefWarehouseId || cachedWarehouseId || preferredWarehouseFallback || "",
486
563
  });
@@ -795,20 +872,94 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
795
872
  }, 50);
796
873
  return () => window.clearTimeout(id);
797
874
  }, [step]);
798
- const submit = async () => {
875
+ // ---- Publiceren -----------------------------------------------------
876
+ // Elke publicatie is één opdracht met vaste deelstappen. Geslaagde stappen
877
+ // worden bij opnieuw proberen overgeslagen, tijdelijke storingen worden
878
+ // automatisch herhaald en alles is terug te zien bij "Publicaties".
879
+ const characteristicLabels = useMemo(() => {
880
+ const out = {};
881
+ for (const c of spec.characteristics)
882
+ out[c.code] = c.label || c.code;
883
+ return out;
884
+ }, [spec.characteristics]);
885
+ const wantsPublish = (isEditingPublished && Boolean(tradeItemId)) ||
886
+ (!isEditingPublished && publishToFloriday);
887
+ const readiness = useMemo(() => buildReadinessReport({
888
+ data,
889
+ spec,
890
+ connectionOk: Boolean(activeId) && connectionReadiness.data?.ready === true,
891
+ connectionLoading: connLoading || connectionReadiness.isLoading,
892
+ environment: activeConnection?.active_environment ?? null,
893
+ connectionMessage: connectionReadiness.data?.message ?? null,
894
+ schemaReady: schemaQuery.isSuccess,
895
+ learnedCharacteristicCodes: serverRequiredCodes,
896
+ characteristicLabels,
897
+ wantsPublish,
898
+ }), [
899
+ data,
900
+ spec,
901
+ activeId,
902
+ connLoading,
903
+ connectionReadiness.data,
904
+ connectionReadiness.isLoading,
905
+ activeConnection?.active_environment,
906
+ schemaQuery.isSuccess,
907
+ serverRequiredCodes,
908
+ characteristicLabels,
909
+ wantsPublish,
910
+ ]);
911
+ const goToStepKey = useCallback((key) => {
912
+ const idx = visibleSteps(data).findIndex((s) => s.key === key);
913
+ if (idx >= 0)
914
+ setStep(idx);
915
+ }, [data]);
916
+ const handleStepFailure = useCallback(async (friendly) => {
917
+ if (!friendly)
918
+ return;
919
+ if (friendly.code === "MISSING_CHARACTERISTICS" && friendly.missingCodes?.length) {
920
+ if (data.vbnCode) {
921
+ try {
922
+ const res = await saveFloridayVbnRequired({
923
+ data: { vbnCode: data.vbnCode, codes: friendly.missingCodes },
924
+ });
925
+ queryClient.setQueryData(["floriday-vbn-required", data.vbnCode], {
926
+ vbnCode: res.vbnCode,
927
+ requiredCodes: res.requiredCodes,
928
+ });
929
+ }
930
+ catch {
931
+ /* leren mag stilletjes falen */
932
+ }
933
+ void rememberPublishRejection({
934
+ vbnCode: data.vbnCode,
935
+ scope: "characteristic",
936
+ keys: friendly.missingCodes,
937
+ lastError: friendly.detail,
938
+ });
939
+ }
940
+ goToStepKey("eigenschappen");
941
+ }
942
+ else if (friendly.action?.stepKey) {
943
+ goToStepKey(friendly.action.stepKey);
944
+ }
945
+ toast.error(friendly.message, {
946
+ description: friendly.hint,
947
+ });
948
+ }, [data.vbnCode, goToStepKey, queryClient]);
949
+ const submit = async (options) => {
799
950
  if (!activeId) {
800
- toast.error("Geen actieve Floriday-connectie geselecteerd");
951
+ toast.error("Geen actieve verbinding met de handelsomgeving", {
952
+ description: "Stel de verbinding in bij Instellingen.",
953
+ });
801
954
  return;
802
955
  }
803
- // Publish-gate: blokkeer zolang het trade-item schema niet succesvol
804
- // geladen is — anders kunnen verplichte karakteristieken ontbreken.
805
- const wantsPublish = (isEditingPublished && Boolean(tradeItemId)) ||
806
- (!isEditingPublished && publishToFloriday);
807
956
  if (wantsPublish && !schemaQuery.isSuccess) {
808
- toast.error("Floriday-schema nog niet geladen — wacht even of probeer opnieuw.");
957
+ toast.error("De productregels worden nog opgehaald", {
958
+ description: "Wacht heel even en probeer het opnieuw.",
959
+ });
809
960
  return;
810
961
  }
811
- // Strict valideer alle stappen voor verzending
962
+ // Strikte controle van alle stappen voor verzending.
812
963
  for (const s of steps) {
813
964
  const v = validateStepLocal(s.key, data);
814
965
  if (!v.ok) {
@@ -819,68 +970,226 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
819
970
  return;
820
971
  }
821
972
  }
973
+ // Lokaal bewaren (niet publiceren) blijft een simpele actie zonder opdracht.
974
+ if (!wantsPublish) {
975
+ setSubmitting(true);
976
+ try {
977
+ const newId = await upsertLocalTradeItem({
978
+ id: draftId,
979
+ connectionId: activeId,
980
+ data,
981
+ });
982
+ setDraftId(newId);
983
+ toast.success(draftId
984
+ ? "Wijzigingen opgeslagen (nog niet gepubliceerd)"
985
+ : "Artikel opgeslagen (nog niet gepubliceerd)");
986
+ navigate({ to: "/voorraad" });
987
+ }
988
+ catch (e) {
989
+ const friendly = translatePublishError(e);
990
+ toast.error(friendly.message, { description: friendly.hint });
991
+ }
992
+ finally {
993
+ setSubmitting(false);
994
+ }
995
+ return;
996
+ }
997
+ // Publiceren begint altijd met een lokale opslag, zodat een afgebroken of
998
+ // mislukte publicatie nooit tot verlies van ingevoerde gegevens leidt.
999
+ try {
1000
+ const localId = await upsertLocalTradeItem({
1001
+ id: draftId,
1002
+ connectionId: activeId,
1003
+ data,
1004
+ floridayTradeItemId: (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null),
1005
+ sync: { dirty: true, last_error: null },
1006
+ });
1007
+ if (localId && localId !== draftId)
1008
+ setDraftId(localId);
1009
+ }
1010
+ catch (e) {
1011
+ toast.error(`Lokaal opslaan mislukt: ${e.message}`);
1012
+ return;
1013
+ }
1014
+ try {
1015
+ const serverConnection = await getFloridayReadinessFn();
1016
+ const browserEnvironment = activeConnection?.active_environment;
1017
+ if (!serverConnection.ready) {
1018
+ toast.error(serverConnection.message, {
1019
+ description: "Controleer de verbinding bij Instellingen.",
1020
+ });
1021
+ return;
1022
+ }
1023
+ if (serverConnection.connectionId !== activeId ||
1024
+ (browserEnvironment && serverConnection.environment !== browserEnvironment)) {
1025
+ toast.error("De verbinding is gewijzigd", {
1026
+ description: "Vernieuw de pagina en probeer opnieuw.",
1027
+ });
1028
+ return;
1029
+ }
1030
+ }
1031
+ catch {
1032
+ toast.error("De verbinding kan niet worden gecontroleerd", {
1033
+ description: "Vernieuw de pagina en probeer opnieuw.",
1034
+ });
1035
+ return;
1036
+ }
822
1037
  setSubmitting(true);
1038
+ const environment = activeConnection?.active_environment;
1039
+ if (environment !== "staging" && environment !== "live") {
1040
+ toast.error("De gekozen omgeving is niet geldig", {
1041
+ description: "Kies de testomgeving of liveomgeving bij Instellingen.",
1042
+ });
1043
+ setSubmitting(false);
1044
+ return;
1045
+ }
1046
+ const intent = isEditingPublished ? "update" : "create";
1047
+ const activeStepKeys = ["trade-item", "verify"];
1048
+ if (data.offerForSale) {
1049
+ activeStepKeys.push("stock", "offer", "supply-line", "prices");
1050
+ }
1051
+ else if (isEditingPublished) {
1052
+ activeStepKeys.push("prices");
1053
+ }
1054
+ let jobId = options?.resumeJobId ?? null;
1055
+ if (!jobId) {
1056
+ const job = await createPublishJob({
1057
+ connectionId: activeId,
1058
+ environment,
1059
+ intent,
1060
+ title: data.name || data.vbnName || "Product",
1061
+ productId: productId ?? null,
1062
+ tradeItemId: tradeItemId ?? null,
1063
+ snapshot: { articleCode: data.articleCode, vbnCode: data.vbnCode },
1064
+ activeSteps: activeStepKeys,
1065
+ });
1066
+ jobId = job?.id ?? null;
1067
+ }
1068
+ setPublishJobId(jobId);
1069
+ const stepOpts = { characteristicLabels };
1070
+ let resolvedTradeItemId = tradeItemId ?? null;
823
1071
  try {
824
- if (isEditingPublished && tradeItemId) {
825
- await saveWarehousePreference(tradeItemId, data.warehouseId);
826
- // PUT vervangt het bestaande trade item in Floriday
827
- // PUT /trade-items/{id} — body volgens TradeItemUpdate (zonder tradeItemId + supplierArticleCode)
828
- const payload = buildUpdateTradeItemPayload(data);
829
- const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
830
- if (!res?.attemptId) {
831
- throw new Error("Gateway gaf geen attemptId terug — wijziging niet bevestigd");
832
- }
833
- // Optimistisch: cache-rij meteen bijwerken zodat de tabel niet 5 min wacht
834
- try {
835
- const cachePayload = {
836
- ...buildAddTradeItemPayload(data),
837
- tradeItemId,
838
- };
839
- await supabase
840
- .from("floriday_trade_items_cache")
841
- .update({
842
- data: cachePayload,
843
- fetched_at: new Date().toISOString(),
844
- })
845
- .eq("connection_id", activeId)
846
- .eq("floriday_id", tradeItemId);
1072
+ // 1) Productgegevens -------------------------------------------------
1073
+ const core = await runPublishStep(jobId, "trade-item", async () => {
1074
+ if (isEditingPublished && tradeItemId) {
1075
+ await saveWarehousePreference(tradeItemId, data.warehouseId);
1076
+ const payload = buildUpdateTradeItemPayload(data);
1077
+ const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
1078
+ if (!res?.attemptId) {
1079
+ throw new Error("De wijziging is niet bevestigd door de handelsomgeving");
1080
+ }
1081
+ return { tradeItemId };
847
1082
  }
848
- catch {
849
- /* niet kritiek, sync corrigeert binnen 5 min */
1083
+ const payload = buildAddTradeItemPayload(data);
1084
+ const res = await floriday.createTradeItem(activeId, payload);
1085
+ if (!res?.attemptId) {
1086
+ throw new Error("Het aanmaken is niet bevestigd door de handelsomgeving");
850
1087
  }
851
- toast.success("Artikel bijgewerkt in Floriday");
852
- // Herprijzen: als er een prijs is ingevuld, supply-line (opnieuw) pushen.
853
- if (data.offerForSale && data.salePricePerPiece > 0) {
1088
+ const createdId = res.tradeItemId ?? payload.tradeItemId;
1089
+ await saveWarehousePreference(createdId, data.warehouseId);
1090
+ if (draftId) {
854
1091
  try {
855
- const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
856
- data.packingConfigurations[0];
857
- if (primary?.id) {
858
- await upsertSupplyLineForTradeItem({
859
- connectionId: activeId,
860
- tradeItemId,
861
- pricePerPiece: data.salePricePerPiece,
862
- packingConfigurationId: primary.id,
863
- availableQuantity: data.saleStockPieces,
864
- customerOrganizationIds: data.customerOrganizationIds,
865
- periodStart: data.offerStartDate
866
- ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
867
- : undefined,
868
- });
869
- toast.success("Prijs bijgewerkt in Floriday-catalogus");
870
- }
1092
+ await deleteLocalTradeItem(draftId);
871
1093
  }
872
- catch (e) {
873
- toast.error(`Prijs bijwerken mislukt: ${e.message}`);
1094
+ catch {
1095
+ /* niet kritiek */
874
1096
  }
875
1097
  }
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];
1098
+ return { tradeItemId: createdId };
1099
+ }, stepOpts);
1100
+ if (!core.ok) {
1101
+ await handleStepFailure(core.error);
1102
+ if (jobId)
1103
+ await finishPublishJob(jobId);
1104
+ return;
1105
+ }
1106
+ resolvedTradeItemId =
1107
+ core.value?.tradeItemId ??
1108
+ resolvedTradeItemId;
1109
+ const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
1110
+ data.packingConfigurations[0];
1111
+ // 2) Voorraad ---------------------------------------------------------
1112
+ if (activeStepKeys.includes("stock") && resolvedTradeItemId) {
1113
+ await runPublishStep(jobId, "stock", async () => {
1114
+ const key = stockKeyFor({ tradeItemId: resolvedTradeItemId });
1115
+ if (!key || !(data.saleStockPieces > 0))
1116
+ return { skipped: true };
1117
+ const { data: existing } = await supabase
1118
+ .from("trade_item_stock")
1119
+ .select("trade_item_key")
1120
+ .eq("connection_id", activeId)
1121
+ .eq("trade_item_key", key)
1122
+ .maybeSingle();
1123
+ if (existing)
1124
+ return { kept: true };
1125
+ const userRes = await supabase.auth.getUser();
1126
+ const userId = userRes.data.user?.id;
1127
+ if (!userId)
1128
+ throw new Error("Unauthorized");
1129
+ const { error } = await supabase.from("trade_item_stock").insert({
1130
+ user_id: userId,
1131
+ connection_id: activeId,
1132
+ trade_item_key: key,
1133
+ physical_stock: data.saleStockPieces,
1134
+ minimum_stock: 0,
1135
+ reserved_stock: 0,
1136
+ incoming_stock: 0,
1137
+ });
1138
+ if (error)
1139
+ throw new Error(error.message);
1140
+ return { created: true };
1141
+ }, stepOpts);
1142
+ }
1143
+ // 3) Aanbod -----------------------------------------------------------
1144
+ if (activeStepKeys.includes("offer") && resolvedTradeItemId && !isEditingPublished) {
1145
+ await runPublishStep(jobId, "offer", async () => {
1146
+ const packingBase = buildPrimaryPackingBase(data);
1147
+ await publishCustomerOfferFn({
1148
+ data: {
1149
+ customerOfferId: crypto.randomUUID(),
1150
+ tradeItemId: resolvedTradeItemId,
1151
+ title: data.name || data.vbnName || "Aanbod",
1152
+ description: "",
1153
+ pricePerPiece: data.salePricePerPiece,
1154
+ currency: "EUR",
1155
+ despatchWarehouseId: data.warehouseId,
1156
+ numberOfPieces: data.saleStockPieces,
1157
+ allowedCustomerOrganizationIds: data.customerOrganizationIds,
1158
+ packingConfiguration: packingBase,
1159
+ orderStart: new Date(`${data.offerStartDate}T00:00:00Z`).toISOString(),
1160
+ },
1161
+ });
1162
+ return { published: true };
1163
+ }, stepOpts);
1164
+ }
1165
+ else if (jobId && activeStepKeys.includes("offer")) {
1166
+ await markStepSkipped(jobId, "offer");
1167
+ }
1168
+ // 4) Prijs online ------------------------------------------------------
1169
+ if (activeStepKeys.includes("supply-line") && resolvedTradeItemId) {
1170
+ await runPublishStep(jobId, "supply-line", async () => {
1171
+ if (!primary?.id || !(data.salePricePerPiece > 0))
1172
+ return { skipped: true };
1173
+ const sl = await upsertSupplyLineForTradeItem({
1174
+ connectionId: activeId,
1175
+ tradeItemId: resolvedTradeItemId,
1176
+ pricePerPiece: data.salePricePerPiece,
1177
+ packingConfigurationId: primary.id,
1178
+ availableQuantity: data.saleStockPieces,
1179
+ customerOrganizationIds: data.customerOrganizationIds,
1180
+ periodStart: data.offerStartDate
1181
+ ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
1182
+ : undefined,
1183
+ });
1184
+ return { supplyLineId: sl.supplyLineId ?? null };
1185
+ }, stepOpts);
1186
+ }
1187
+ // 5) Prijsafspraken ----------------------------------------------------
1188
+ if (activeStepKeys.includes("prices") && resolvedTradeItemId) {
1189
+ await runPublishStep(jobId, "prices", async () => {
881
1190
  const priceSync = await syncTradeItemPrices({
882
- tradeItemId,
883
- tradeItemKey: tradeItemId,
1191
+ tradeItemId: resolvedTradeItemId,
1192
+ tradeItemKey: resolvedTradeItemId,
884
1193
  availableQuantity: data.saleStockPieces,
885
1194
  currency: "EUR",
886
1195
  periodStart: data.offerStartDate
@@ -893,199 +1202,82 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
893
1202
  });
894
1203
  const failed = priceSync.groupResults.filter((r) => !r.ok);
895
1204
  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`);
1205
+ throw new Error(`Deze prijsafspraken zijn niet doorgekomen: ${failed
1206
+ .map((r) => r.name)
1207
+ .join(", ")}`);
900
1208
  }
901
- }
902
- catch (e) {
903
- toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
904
- }
1209
+ return { groups: priceSync.groupResults.length };
1210
+ }, stepOpts);
905
1211
  }
906
- else if (publishToFloriday) {
907
- const payload = buildAddTradeItemPayload(data);
908
- const res = await floriday.createTradeItem(activeId, payload);
909
- if (!res?.attemptId) {
910
- throw new Error("Gateway gaf geen attemptId terug — aanmaken niet bevestigd");
911
- }
912
- const createdTradeItemId = res.tradeItemId ?? payload.tradeItemId;
913
- await saveWarehousePreference(createdTradeItemId, data.warehouseId);
914
- // Als dit een lokaal artikel was, ruim de draft-rij op
915
- if (draftId) {
916
- try {
917
- await deleteLocalTradeItem(draftId);
1212
+ // 6) Controle achteraf --------------------------------------------------
1213
+ if (resolvedTradeItemId) {
1214
+ const verify = await runPublishStep(jobId, "verify", async () => {
1215
+ const res = (await gatewayReadFn({
1216
+ data: {
1217
+ path: `/suppliers-api-2026v1/trade-items/${resolvedTradeItemId}`,
1218
+ },
1219
+ }));
1220
+ const item = res?.data;
1221
+ if (!item) {
1222
+ throw new Error("Het product kon niet worden teruggelezen");
918
1223
  }
919
- catch {
920
- // Niet kritiek; rij verdwijnt na volgende reload
921
- }
922
- }
923
- // ---- Direct-sale flow -----------------------------------------
924
- // Alleen doorzetten als de gebruiker de "Aanbieden"-stap geactiveerd
925
- // heeft. Falen mag de TradeItem-succes NIET terugdraaien — daarom
926
- // in een aparte try/catch en apart toast bericht.
927
- if (data.offerForSale) {
928
- const tradeItemId = createdTradeItemId;
929
- // 1) Seed lokale voorraad (physical_stock) — de continuous-stock
930
- // trigger pusht dit naar Floriday zodra de trade item in de
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.
937
- try {
938
- const key = stockKeyFor({ tradeItemId });
939
- if (key && data.saleStockPieces > 0) {
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
- }
962
- }
963
- }
964
- }
965
- catch (e) {
966
- toast.error(`Voorraad opslaan mislukt: ${e.message}`);
967
- }
968
- // 2) CustomerOffer publiceren (POST /customer-offers)
969
- try {
970
- const packingBase = buildPrimaryPackingBase(data);
971
- await publishCustomerOfferFn({
972
- data: {
973
- customerOfferId: crypto.randomUUID(),
974
- tradeItemId,
975
- title: data.name || data.vbnName || "Aanbod",
976
- description: "",
977
- pricePerPiece: data.salePricePerPiece,
978
- currency: "EUR",
979
- despatchWarehouseId: data.warehouseId,
980
- numberOfPieces: data.saleStockPieces,
981
- allowedCustomerOrganizationIds: data.customerOrganizationIds,
982
- packingConfiguration: packingBase,
983
- orderStart: new Date(`${data.offerStartDate}T00:00:00Z`).toISOString(),
984
- },
985
- });
986
- toast.success("Artikel aangemaakt en aangeboden in Floriday");
987
- }
988
- catch (e) {
989
- toast.error(`Aanbod publiceren mislukt: ${e.message}`);
990
- }
991
- // 3) Supply-line pushen naar de Authenticatie-module gateway
992
- // zodat het product met prijs online komt in de Floriday-catalogus.
993
- // Idempotent op supplyLineId — herprijzen hergebruikt dezelfde regel.
1224
+ // Lokale kopie pas bijwerken nadat de handelsomgeving bevestigd heeft.
994
1225
  try {
995
- const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
996
- data.packingConfigurations[0];
997
- if (primary?.id && data.salePricePerPiece > 0) {
998
- const sl = await upsertSupplyLineForTradeItem({
999
- connectionId: activeId,
1000
- tradeItemId,
1001
- pricePerPiece: data.salePricePerPiece,
1002
- packingConfigurationId: primary.id,
1003
- availableQuantity: data.saleStockPieces,
1004
- customerOrganizationIds: data.customerOrganizationIds,
1005
- periodStart: data.offerStartDate
1006
- ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
1007
- : undefined,
1008
- });
1009
- if (sl.supplyLineId) {
1010
- toast.success("Prijs online gezet in Floriday-catalogus");
1011
- }
1012
- }
1013
- }
1014
- catch (e) {
1015
- toast.error(`Prijs online zetten mislukt: ${e.message}`);
1226
+ await supabase
1227
+ .from("floriday_trade_items_cache")
1228
+ .update({
1229
+ data: item,
1230
+ fetched_at: new Date().toISOString(),
1231
+ })
1232
+ .eq("connection_id", activeId)
1233
+ .eq("floriday_id", resolvedTradeItemId);
1016
1234
  }
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
- }
1235
+ catch {
1236
+ /* wordt automatisch bijgewerkt bij de volgende synchronisatie */
1038
1237
  }
1039
- catch (e) {
1040
- toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
1238
+ const warnings = [];
1239
+ const remoteName = item?.tradeItemName ?? item?.name;
1240
+ if (remoteName && data.name && remoteName !== data.name) {
1241
+ warnings.push("De artikelnaam wijkt af");
1041
1242
  }
1243
+ return { warnings };
1244
+ }, stepOpts);
1245
+ if (!verify.ok) {
1246
+ toast.warning("Het product is opgeslagen, maar nog niet bevestigd", {
1247
+ description: "Controleer het later bij Publicaties.",
1248
+ });
1042
1249
  }
1043
- else {
1044
- toast.success("Artikel aangemaakt in Floriday");
1045
- }
1046
- }
1047
- else {
1048
- const newId = await upsertLocalTradeItem({
1049
- id: draftId,
1050
- connectionId: activeId,
1051
- data,
1052
- });
1053
- setDraftId(newId);
1054
- toast.success(draftId
1055
- ? "Wijzigingen opgeslagen (nog niet gepubliceerd)"
1056
- : "Artikel opgeslagen (nog niet gepubliceerd)");
1057
1250
  }
1058
- navigate({ to: "/voorraad" });
1059
- }
1060
- catch (e) {
1061
- const msg = e.message;
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 */ }
1251
+ const status = jobId ? await finishPublishJob(jobId, {
1252
+ tradeItemId: resolvedTradeItemId,
1253
+ }) : "succeeded";
1254
+ if (status === "succeeded") {
1255
+ if (draftId) {
1256
+ await markLocalSyncStatus(draftId, {
1257
+ dirty: false,
1258
+ last_error: null,
1259
+ last_synced_at: new Date().toISOString(),
1260
+ }).catch(() => { });
1076
1261
  }
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.`);
1262
+ setPendingSync(null);
1263
+ toast.success(isEditingPublished ? "Product bijgewerkt" : "Product gepubliceerd");
1264
+ navigate({ to: "/voorraad" });
1081
1265
  }
1082
- else if (msg.includes("NO_ACTIVE_FLORIDAY_CONNECTION")) {
1083
- toast.error("Geen actieve Floriday-connectie. Ga naar Instellingen → Floriday.");
1266
+ else if (status === "succeeded_with_warnings") {
1267
+ toast.warning("Gepubliceerd, met aandachtspunten", {
1268
+ description: "Bekijk wat er nog moet gebeuren in het overzicht hieronder.",
1269
+ });
1084
1270
  }
1085
1271
  else {
1086
- toast.error(`Opslaan mislukt: ${msg}`);
1272
+ toast.error("Publiceren is niet gelukt");
1087
1273
  }
1088
1274
  }
1275
+ catch (e) {
1276
+ const friendly = translatePublishError(e, { characteristicLabels });
1277
+ await handleStepFailure(friendly);
1278
+ if (jobId)
1279
+ await finishPublishJob(jobId);
1280
+ }
1089
1281
  finally {
1090
1282
  setSubmitting(false);
1091
1283
  }
@@ -1138,12 +1330,113 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1138
1330
  setSubmitting(false);
1139
1331
  }
1140
1332
  };
1141
- // Auto-save: zodra er een bestaand concept OF een gepubliceerd artikel in
1142
- // bewerking is, persisteren we wijzigingen debounced. Voor concepten gaan de
1143
- // wijzigingen naar `trade_item_drafts`; voor gepubliceerde artikelen wordt
1144
- // een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
1145
- // meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
1146
- // het systeem beschikbaar zijn en niet verloren raken bij navigatie.
1333
+ // Auto-save: wijzigingen worden ALTIJD eerst lokaal vastgelegd en pas daarna
1334
+ // naar Floriday gestuurd. Mislukt die push, dan blijft de invoer bewaard en
1335
+ // wordt het artikel gemarkeerd als "nog niet doorgegeven"; bij het opnieuw
1336
+ // openen is de lokale versie dan leidend.
1337
+ const persistAndPush = useCallback(async (snapshot) => {
1338
+ if (!activeId)
1339
+ return false;
1340
+ const pushTargetId = (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null);
1341
+ setSyncState("saving");
1342
+ setSyncError(null);
1343
+ // 1. Lokaal opslaan — nooit overslaan.
1344
+ let localId = draftId;
1345
+ try {
1346
+ localId = await upsertLocalTradeItem({
1347
+ id: draftId,
1348
+ connectionId: activeId,
1349
+ data: snapshot,
1350
+ floridayTradeItemId: pushTargetId,
1351
+ sync: { dirty: Boolean(pushTargetId), last_error: null },
1352
+ });
1353
+ if (localId && localId !== draftId)
1354
+ setDraftId(localId);
1355
+ }
1356
+ catch (e) {
1357
+ const msg = e.message || "Onbekende fout";
1358
+ setSyncError(msg);
1359
+ setSyncState("error");
1360
+ toast.error(`Lokaal opslaan mislukt: ${msg}`);
1361
+ return false;
1362
+ }
1363
+ const finishLocalOnly = () => {
1364
+ setSyncState("local");
1365
+ return true;
1366
+ };
1367
+ if (!pushTargetId) {
1368
+ setPendingSync(null);
1369
+ if (localId) {
1370
+ void markLocalSyncStatus(localId, {
1371
+ dirty: false,
1372
+ last_synced_at: null,
1373
+ }).catch(() => { });
1374
+ }
1375
+ return finishLocalOnly();
1376
+ }
1377
+ if (!snapshot.productGroup)
1378
+ return finishLocalOnly();
1379
+ // Geen warehouse gekozen? Alleen lokaal bewaren; zodra de gebruiker een
1380
+ // magazijn kiest gaat de sync verder.
1381
+ const whId = (snapshot.warehouseId ?? "").trim();
1382
+ 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);
1383
+ if (!isUuid)
1384
+ return finishLocalOnly();
1385
+ // 2. Doorgeven aan Floriday.
1386
+ try {
1387
+ await saveWarehousePreference(pushTargetId, snapshot.warehouseId);
1388
+ const payload = buildUpdateTradeItemPayload(snapshot);
1389
+ const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
1390
+ if (!res?.attemptId) {
1391
+ throw new Error("Gateway gaf geen attemptId terug");
1392
+ }
1393
+ try {
1394
+ const cachePayload = {
1395
+ ...buildAddTradeItemPayload(snapshot),
1396
+ tradeItemId: pushTargetId,
1397
+ };
1398
+ await supabase
1399
+ .from("floriday_trade_items_cache")
1400
+ .update({
1401
+ data: cachePayload,
1402
+ fetched_at: new Date().toISOString(),
1403
+ })
1404
+ .eq("connection_id", activeId)
1405
+ .eq("floriday_id", pushTargetId);
1406
+ }
1407
+ catch {
1408
+ /* niet kritiek; sync corrigeert */
1409
+ }
1410
+ if (localId) {
1411
+ await markLocalSyncStatus(localId, {
1412
+ dirty: false,
1413
+ last_error: null,
1414
+ last_synced_at: new Date().toISOString(),
1415
+ }).catch(() => { });
1416
+ }
1417
+ setPendingSync(null);
1418
+ setSyncState("saved");
1419
+ return true;
1420
+ }
1421
+ catch (e) {
1422
+ const msg = e.message || "Onbekende fout";
1423
+ if (localId) {
1424
+ void markLocalSyncStatus(localId, { dirty: true, last_error: msg }).catch(() => { });
1425
+ }
1426
+ setPendingSync({ error: msg, at: new Date().toISOString() });
1427
+ setSyncError(msg);
1428
+ setSyncState("error");
1429
+ return false;
1430
+ }
1431
+ }, [
1432
+ activeId,
1433
+ draftId,
1434
+ draftFloridayId,
1435
+ isEditingPublished,
1436
+ tradeItemId,
1437
+ saveWarehousePreference,
1438
+ floriday,
1439
+ ]);
1147
1440
  useEffect(() => {
1148
1441
  if (!activeId)
1149
1442
  return;
@@ -1162,66 +1455,12 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1162
1455
  }
1163
1456
  if (lastSavedRef.current === serialized)
1164
1457
  return;
1165
- const t = window.setTimeout(async () => {
1166
- setSyncState("saving");
1167
- setSyncError(null);
1168
- try {
1169
- // Altijd lokaal opslaan als we een draft-rij hebben
1170
- if (draftId) {
1171
- await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
1172
- }
1173
- if (pushTargetId && !data.productGroup) {
1174
- setSyncState(draftId ? "local" : "idle");
1175
- lastSavedRef.current = serialized;
1176
- return;
1177
- }
1178
- // Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
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
- }
1189
- await saveWarehousePreference(pushTargetId, data.warehouseId);
1190
- const payload = buildUpdateTradeItemPayload(data);
1191
- const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
1192
- if (!res?.attemptId) {
1193
- throw new Error("Gateway gaf geen attemptId terug");
1194
- }
1195
- try {
1196
- const cachePayload = {
1197
- ...buildAddTradeItemPayload(data),
1198
- tradeItemId: pushTargetId,
1199
- };
1200
- await supabase
1201
- .from("floriday_trade_items_cache")
1202
- .update({
1203
- data: cachePayload,
1204
- fetched_at: new Date().toISOString(),
1205
- })
1206
- .eq("connection_id", activeId)
1207
- .eq("floriday_id", pushTargetId);
1208
- }
1209
- catch {
1210
- /* niet kritiek; sync corrigeert */
1211
- }
1212
- setSyncState("saved");
1213
- }
1214
- else {
1215
- setSyncState("local");
1216
- }
1217
- lastSavedRef.current = serialized;
1218
- }
1219
- catch (e) {
1220
- const msg = e.message || "Onbekende fout";
1221
- setSyncError(msg);
1222
- setSyncState("error");
1223
- toast.error(`Sync naar Floriday mislukt: ${msg}`);
1224
- }
1458
+ const snapshot = data;
1459
+ const t = window.setTimeout(() => {
1460
+ // De lokale opslag slaagt (bijna) altijd; de baseline mag dus mee, ook als
1461
+ // de Floriday-push faalt — de invoer staat dan veilig in de eigen database.
1462
+ lastSavedRef.current = serialized;
1463
+ void persistAndPush(snapshot);
1225
1464
  }, 800);
1226
1465
  return () => window.clearTimeout(t);
1227
1466
  }, [
@@ -1233,9 +1472,22 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1233
1472
  publishedLoading,
1234
1473
  productLoading,
1235
1474
  isEditingPublished,
1236
- saveWarehousePreference,
1237
1475
  tradeItemId,
1476
+ persistAndPush,
1238
1477
  ]);
1478
+ const retrySync = useCallback(async () => {
1479
+ setRetryingSync(true);
1480
+ try {
1481
+ const ok = await persistAndPush(data);
1482
+ if (ok)
1483
+ toast.success("Wijzigingen doorgegeven aan Floriday");
1484
+ else
1485
+ toast.error("Doorgeven aan Floriday lukt nog niet; je gegevens blijven bewaard");
1486
+ }
1487
+ finally {
1488
+ setRetryingSync(false);
1489
+ }
1490
+ }, [persistAndPush, data]);
1239
1491
  if (connLoading || draftLoading || publishedLoading || productLoading) {
1240
1492
  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
1241
1493
  ? "Artikel laden..."
@@ -1252,7 +1504,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1252
1504
  setDeleteOpen(o);
1253
1505
  if (!o)
1254
1506
  setDeleteAck(false);
1255
- }, children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Product verwijderen?" }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { children: ["Je staat op het punt om ", _jsx("strong", { children: data.name || "dit artikel" }), data.articleCode ? _jsxs(_Fragment, { children: [" (artikelnummer ", _jsx("code", { children: data.articleCode }), ")"] }) : null, " ", "lokaal uit de voorraad te verwijderen."] }), _jsxs("div", { className: "rounded-md border p-3 bg-muted/40 space-y-1", children: [_jsxs("div", { children: ["Huidige voorraad: ", _jsx("strong", { children: productMeta.physicalStock }), " stuks"] }), _jsxs("div", { children: ["Floriday-koppeling:", " ", productMeta.floridayActive || productMeta.floridayTradeItemId ? (_jsx("span", { className: "text-emerald-700 font-medium", children: "actief" })) : (_jsx("span", { className: "text-muted-foreground", children: "geen" }))] })] }), (productMeta.floridayActive || productMeta.floridayTradeItemId) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900", children: [_jsx("strong", { children: "Let op:" }), " dit artikel is gekoppeld met Floriday. Als je alleen de lokale kant verwijdert, blijft het artikel op Floriday bestaan en verschijnt het bij de volgende sync weer terug in de lijst. Trek het artikel daarna zelf in via het Floriday-portaal als je het echt weg wilt."] })), productMeta.physicalStock > 0 && (_jsxs("div", { className: "rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-900", children: ["Er staat nog voorraad (", productMeta.physicalStock, " stuks) op dit product. Deze voorraad wordt \u00F3\u00F3k verwijderd en kan niet automatisch worden hersteld."] })), _jsxs("label", { className: "flex items-start gap-2 pt-1", children: [_jsx(Checkbox, { checked: deleteAck, onCheckedChange: (v) => setDeleteAck(Boolean(v)) }), _jsx("span", { children: "Ik weet zeker dat ik dit product wil verwijderen en begrijp de gevolgen." })] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { disabled: !deleteAck, onClick: handleDeleteDraft, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Definitief verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsxs(_Fragment, { children: [_jsx(ArtikelCompositionCard, { productId: linkedProductId, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1507
+ }, children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Product verwijderen?" }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { children: ["Je staat op het punt om ", _jsx("strong", { children: data.name || "dit artikel" }), data.articleCode ? _jsxs(_Fragment, { children: [" (artikelnummer ", _jsx("code", { children: data.articleCode }), ")"] }) : null, " ", "lokaal uit de voorraad te verwijderen."] }), _jsxs("div", { className: "rounded-md border p-3 bg-muted/40 space-y-1", children: [_jsxs("div", { children: ["Huidige voorraad: ", _jsx("strong", { children: productMeta.physicalStock }), " stuks"] }), _jsxs("div", { children: ["Floriday-koppeling:", " ", productMeta.floridayActive || productMeta.floridayTradeItemId ? (_jsx("span", { className: "text-emerald-700 font-medium", children: "actief" })) : (_jsx("span", { className: "text-muted-foreground", children: "geen" }))] })] }), (productMeta.floridayActive || productMeta.floridayTradeItemId) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900", children: [_jsx("strong", { children: "Let op:" }), " dit artikel is gekoppeld met Floriday. Als je alleen de lokale kant verwijdert, blijft het artikel op Floriday bestaan en verschijnt het bij de volgende sync weer terug in de lijst. Trek het artikel daarna zelf in via het Floriday-portaal als je het echt weg wilt."] })), productMeta.physicalStock > 0 && (_jsxs("div", { className: "rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-900", children: ["Er staat nog voorraad (", productMeta.physicalStock, " stuks) op dit product. Deze voorraad wordt \u00F3\u00F3k verwijderd en kan niet automatisch worden hersteld."] })), _jsxs("label", { className: "flex items-start gap-2 pt-1", children: [_jsx(Checkbox, { checked: deleteAck, onCheckedChange: (v) => setDeleteAck(Boolean(v)) }), _jsx("span", { children: "Ik weet zeker dat ik dit product wil verwijderen en begrijp de gevolgen." })] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { disabled: !deleteAck, onClick: handleDeleteDraft, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Definitief verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), pendingSync && (_jsxs(Card, { className: "p-4 border-amber-300 bg-amber-50 text-amber-900 space-y-2", children: [_jsx("div", { className: "font-medium text-sm", children: "Je wijzigingen zijn opgeslagen, maar nog niet doorgegeven aan Floriday" }), _jsxs("div", { className: "text-xs", children: ["Alles wat je hier hebt ingevuld blijft bewaard in dit systeem. Zodra het doorgeven lukt, verdwijnt deze melding vanzelf.", pendingSync.error ? ` Reden: ${pendingSync.error}` : ""] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: retryingSync, onClick: retrySync, children: retryingSync ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin mr-1" }), " Bezig\u2026"] })) : ("Opnieuw proberen") })] })), 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: () => void 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: () => void submit(), disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsxs(_Fragment, { children: [_jsx(ArtikelCompositionCard, { productId: linkedProductId, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1256
1508
  name: data.name || data.vbnName,
1257
1509
  articleNumber: data.articleCode,
1258
1510
  gtin: data.gtin,
@@ -1280,7 +1532,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1280
1532
  ? "border-destructive bg-destructive text-destructive-foreground"
1281
1533
  : "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
1282
1534
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
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
1535
+ : "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) && (_jsx(PublishReadinessCard, { report: readiness, loading: connLoading || connectionReadiness.isLoading || schemaQuery.isLoading, onGoToStep: goToStepKey })), publishJob && (_jsx(PublishProgressCard, { job: publishJob, steps: publishSteps, retrying: submitting, onRetry: () => void submit({ resumeJobId: publishJob.id }), onFixStep: (key) => goToStepKey(key === "trade-item" ? "eigenschappen" : "aanbod") })), (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: () => void 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
1284
1536
  ? "Wijzigingen opslaan"
1285
1537
  : publishToFloriday
1286
1538
  ? isEditingDraft