@flowselections/floriday-voorraad 1.0.28 → 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 (107) 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 +555 -316
  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 +3 -3
  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/PublicatiesPage.d.ts +2 -0
  14. package/dist-lib/components/voorraad/PublicatiesPage.d.ts.map +1 -0
  15. package/dist-lib/components/voorraad/PublicatiesPage.js +60 -0
  16. package/dist-lib/components/voorraad/PublishProgressCard.d.ts +12 -0
  17. package/dist-lib/components/voorraad/PublishProgressCard.d.ts.map +1 -0
  18. package/dist-lib/components/voorraad/PublishProgressCard.js +30 -0
  19. package/dist-lib/components/voorraad/PublishReadinessCard.d.ts +10 -0
  20. package/dist-lib/components/voorraad/PublishReadinessCard.d.ts.map +1 -0
  21. package/dist-lib/components/voorraad/PublishReadinessCard.js +12 -0
  22. package/dist-lib/components/voorraad/StockSyncStatus.d.ts +2 -0
  23. package/dist-lib/components/voorraad/StockSyncStatus.d.ts.map +1 -0
  24. package/dist-lib/components/voorraad/StockSyncStatus.js +23 -0
  25. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  26. package/dist-lib/components/voorraad/VoorraadPage.js +5 -1
  27. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  28. package/dist-lib/components/voorraad/VoorraadTable.js +1 -1
  29. package/dist-lib/hooks/useFusten.d.ts +17 -0
  30. package/dist-lib/hooks/useFusten.d.ts.map +1 -0
  31. package/dist-lib/hooks/useFusten.js +81 -0
  32. package/dist-lib/hooks/useLocalTradeItems.d.ts +14 -4
  33. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  34. package/dist-lib/hooks/useLocalTradeItems.js +83 -5
  35. package/dist-lib/hooks/useLogisticsTasks.d.ts +13 -0
  36. package/dist-lib/hooks/useLogisticsTasks.d.ts.map +1 -0
  37. package/dist-lib/hooks/useLogisticsTasks.js +155 -0
  38. package/dist-lib/hooks/usePublishJob.d.ts +13 -0
  39. package/dist-lib/hooks/usePublishJob.d.ts.map +1 -0
  40. package/dist-lib/hooks/usePublishJob.js +67 -0
  41. package/dist-lib/hooks/useRegisterModuleBaseUrl.d.ts +2 -0
  42. package/dist-lib/hooks/useRegisterModuleBaseUrl.d.ts.map +1 -0
  43. package/dist-lib/hooks/useRegisterModuleBaseUrl.js +31 -0
  44. package/dist-lib/hooks/useStockSyncHealth.d.ts +8 -0
  45. package/dist-lib/hooks/useStockSyncHealth.d.ts.map +1 -0
  46. package/dist-lib/hooks/useStockSyncHealth.js +42 -0
  47. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
  48. package/dist-lib/hooks/useTradeItemStock.js +37 -1
  49. package/dist-lib/index.d.ts.map +1 -1
  50. package/dist-lib/index.js +5 -1
  51. package/dist-lib/integrations/supabase/auth-middleware.d.ts +1849 -50
  52. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  53. package/dist-lib/integrations/supabase/client.d.ts +1849 -50
  54. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  55. package/dist-lib/integrations/supabase/client.server.d.ts +1849 -50
  56. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  57. package/dist-lib/integrations/supabase/types.d.ts +1870 -41
  58. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  59. package/dist-lib/lib/floricode-required-features.functions.d.ts +1849 -50
  60. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  61. package/dist-lib/lib/floriday-client.d.ts +1 -1
  62. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  63. package/dist-lib/lib/floriday-client.js +1 -2
  64. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +1849 -50
  65. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  66. package/dist-lib/lib/floriday-customer-offer.functions.js +14 -19
  67. package/dist-lib/lib/floriday-env.functions.d.ts +35712 -0
  68. package/dist-lib/lib/floriday-env.functions.d.ts.map +1 -0
  69. package/dist-lib/lib/floriday-env.functions.js +16 -0
  70. package/dist-lib/lib/floriday-env.server.d.ts +12 -0
  71. package/dist-lib/lib/floriday-env.server.d.ts.map +1 -1
  72. package/dist-lib/lib/floriday-env.server.js +15 -2
  73. package/dist-lib/lib/floriday-gateway.functions.d.ts +26934 -82
  74. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  75. package/dist-lib/lib/floriday-gateway.functions.js +12 -3
  76. package/dist-lib/lib/floriday-readiness.server.d.ts +11 -0
  77. package/dist-lib/lib/floriday-readiness.server.d.ts.map +1 -0
  78. package/dist-lib/lib/floriday-readiness.server.js +43 -0
  79. package/dist-lib/lib/floriday-vbn-required.functions.d.ts +3698 -100
  80. package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -1
  81. package/dist-lib/lib/floriday-warehouse.functions.d.ts +5442 -45
  82. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  83. package/dist-lib/lib/floriday-warehouse.functions.js +3 -0
  84. package/dist-lib/lib/floriday-writes.functions.d.ts +3698 -100
  85. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  86. package/dist-lib/lib/fusten.d.ts +87 -0
  87. package/dist-lib/lib/fusten.d.ts.map +1 -0
  88. package/dist-lib/lib/fusten.js +389 -0
  89. package/dist-lib/lib/logistics-engine.d.ts +63 -0
  90. package/dist-lib/lib/logistics-engine.d.ts.map +1 -0
  91. package/dist-lib/lib/logistics-engine.js +57 -0
  92. package/dist-lib/lib/logistics-events.d.ts +18 -0
  93. package/dist-lib/lib/logistics-events.d.ts.map +1 -0
  94. package/dist-lib/lib/logistics-events.js +61 -0
  95. package/dist-lib/lib/module-runtime-config.functions.d.ts +14 -0
  96. package/dist-lib/lib/module-runtime-config.functions.d.ts.map +1 -0
  97. package/dist-lib/lib/module-runtime-config.functions.js +107 -0
  98. package/dist-lib/lib/publish-errors.d.ts +37 -0
  99. package/dist-lib/lib/publish-errors.d.ts.map +1 -0
  100. package/dist-lib/lib/publish-errors.js +183 -0
  101. package/dist-lib/lib/publish-pipeline.d.ts +91 -0
  102. package/dist-lib/lib/publish-pipeline.d.ts.map +1 -0
  103. package/dist-lib/lib/publish-pipeline.js +212 -0
  104. package/dist-lib/lib/publish-readiness.d.ts +42 -0
  105. package/dist-lib/lib/publish-readiness.d.ts.map +1 -0
  106. package/dist-lib/lib/publish-readiness.js +102 -0
  107. 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";
@@ -95,14 +102,23 @@ async function lookupProductGroupForVbn(vbnCode) {
95
102
  .maybeSingle();
96
103
  return data ? groupOfRecord(data) : null;
97
104
  }
98
- export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, } = {}) {
105
+ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, resumeJobId, } = {}) {
99
106
  const navigate = useNavigate();
100
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
+ });
101
115
  const { customers: floridayCustomers } = useFlorydayCustomers(activeId);
102
116
  const { priceGroups } = useFloridayPriceGroups(activeId);
103
117
  const [step, setStep] = useState(0);
104
118
  const [data, setData] = useState(empty);
105
119
  const [submitting, setSubmitting] = useState(false);
120
+ const [publishJobId, setPublishJobId] = useState(resumeJobId ?? null);
121
+ const { job: publishJob, steps: publishSteps } = usePublishJob(publishJobId);
106
122
  const [publishToFloriday, setPublishToFloriday] = useState(true);
107
123
  const [draftId, setDraftId] = useState(initialDraftId);
108
124
  const [draftFloridayId, setDraftFloridayId] = useState(null);
@@ -111,6 +127,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
111
127
  const [productLoading, setProductLoading] = useState(Boolean(productId));
112
128
  const [syncState, setSyncState] = useState("idle");
113
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);
114
133
  const [productMeta, setProductMeta] = useState({ floridayActive: false, floridayTradeItemId: null, physicalStock: 0 });
115
134
  const [deleteAck, setDeleteAck] = useState(false);
116
135
  const [deleteOpen, setDeleteOpen] = useState(false);
@@ -324,6 +343,20 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
324
343
  console.warn("Floriday-cache laden mislukt", e);
325
344
  }
326
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
+ }
327
360
  const cachePackings = cacheData?.packingConfigurations ?? [];
328
361
  const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
329
362
  const seededPacking = cachePackings.length > 0
@@ -486,14 +519,45 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
486
519
  toast.error("Artikel niet gevonden in de cache");
487
520
  return;
488
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
+ }
489
550
  const wizardData = floridayCacheToWizardData(row.data);
490
- const productGroup = wizardData.productGroup ??
491
- (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));
492
556
  const prefWarehouseId = pref?.warehouse_id ?? "";
493
- const cachedWarehouseId = wizardData.warehouseId ?? "";
557
+ const cachedWarehouseId = baseData.warehouseId ?? "";
494
558
  const preferredWarehouseFallback = prefWarehouseId || cachedWarehouseId ? null : await resolvePreferredWarehouseId();
495
559
  setData({
496
- ...wizardData,
560
+ ...baseData,
497
561
  productGroup,
498
562
  warehouseId: prefWarehouseId || cachedWarehouseId || preferredWarehouseFallback || "",
499
563
  });
@@ -808,20 +872,94 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
808
872
  }, 50);
809
873
  return () => window.clearTimeout(id);
810
874
  }, [step]);
811
- 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) => {
812
950
  if (!activeId) {
813
- 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
+ });
814
954
  return;
815
955
  }
816
- // Publish-gate: blokkeer zolang het trade-item schema niet succesvol
817
- // geladen is — anders kunnen verplichte karakteristieken ontbreken.
818
- const wantsPublish = (isEditingPublished && Boolean(tradeItemId)) ||
819
- (!isEditingPublished && publishToFloriday);
820
956
  if (wantsPublish && !schemaQuery.isSuccess) {
821
- 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
+ });
822
960
  return;
823
961
  }
824
- // Strict valideer alle stappen voor verzending
962
+ // Strikte controle van alle stappen voor verzending.
825
963
  for (const s of steps) {
826
964
  const v = validateStepLocal(s.key, data);
827
965
  if (!v.ok) {
@@ -832,68 +970,226 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
832
970
  return;
833
971
  }
834
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
+ }
835
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;
836
1071
  try {
837
- if (isEditingPublished && tradeItemId) {
838
- await saveWarehousePreference(tradeItemId, data.warehouseId);
839
- // PUT vervangt het bestaande trade item in Floriday
840
- // PUT /trade-items/{id} — body volgens TradeItemUpdate (zonder tradeItemId + supplierArticleCode)
841
- const payload = buildUpdateTradeItemPayload(data);
842
- const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
843
- if (!res?.attemptId) {
844
- throw new Error("Gateway gaf geen attemptId terug — wijziging niet bevestigd");
845
- }
846
- // Optimistisch: cache-rij meteen bijwerken zodat de tabel niet 5 min wacht
847
- try {
848
- const cachePayload = {
849
- ...buildAddTradeItemPayload(data),
850
- tradeItemId,
851
- };
852
- await supabase
853
- .from("floriday_trade_items_cache")
854
- .update({
855
- data: cachePayload,
856
- fetched_at: new Date().toISOString(),
857
- })
858
- .eq("connection_id", activeId)
859
- .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 };
860
1082
  }
861
- catch {
862
- /* 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");
863
1087
  }
864
- toast.success("Artikel bijgewerkt in Floriday");
865
- // Herprijzen: als er een prijs is ingevuld, supply-line (opnieuw) pushen.
866
- if (data.offerForSale && data.salePricePerPiece > 0) {
1088
+ const createdId = res.tradeItemId ?? payload.tradeItemId;
1089
+ await saveWarehousePreference(createdId, data.warehouseId);
1090
+ if (draftId) {
867
1091
  try {
868
- const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
869
- data.packingConfigurations[0];
870
- if (primary?.id) {
871
- await upsertSupplyLineForTradeItem({
872
- connectionId: activeId,
873
- tradeItemId,
874
- pricePerPiece: data.salePricePerPiece,
875
- packingConfigurationId: primary.id,
876
- availableQuantity: data.saleStockPieces,
877
- customerOrganizationIds: data.customerOrganizationIds,
878
- periodStart: data.offerStartDate
879
- ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
880
- : undefined,
881
- });
882
- toast.success("Prijs bijgewerkt in Floriday-catalogus");
883
- }
1092
+ await deleteLocalTradeItem(draftId);
884
1093
  }
885
- catch (e) {
886
- toast.error(`Prijs bijwerken mislukt: ${e.message}`);
1094
+ catch {
1095
+ /* niet kritiek */
887
1096
  }
888
1097
  }
889
- // Basisprijs + prijzen per prijsgroep (weekly-base-supply +
890
- // weekly-supply-lines) spiegelen naar trade_item_prices en pushen.
891
- try {
892
- const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
893
- 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 () => {
894
1190
  const priceSync = await syncTradeItemPrices({
895
- tradeItemId,
896
- tradeItemKey: tradeItemId,
1191
+ tradeItemId: resolvedTradeItemId,
1192
+ tradeItemKey: resolvedTradeItemId,
897
1193
  availableQuantity: data.saleStockPieces,
898
1194
  currency: "EUR",
899
1195
  periodStart: data.offerStartDate
@@ -906,199 +1202,82 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
906
1202
  });
907
1203
  const failed = priceSync.groupResults.filter((r) => !r.ok);
908
1204
  if (failed.length) {
909
- toast.error(`Prijsgroep(en) niet gepusht: ${failed.map((r) => r.name).join(", ")}`);
1205
+ throw new Error(`Deze prijsafspraken zijn niet doorgekomen: ${failed
1206
+ .map((r) => r.name)
1207
+ .join(", ")}`);
910
1208
  }
911
- else if (priceSync.groupResults.length) {
912
- toast.success(`${priceSync.groupResults.length} prijsgroep-prijs(zen) bijgewerkt`);
913
- }
914
- }
915
- catch (e) {
916
- toast.error(`Prijzen synchroniseren mislukt: ${e.message}`);
917
- }
1209
+ return { groups: priceSync.groupResults.length };
1210
+ }, stepOpts);
918
1211
  }
919
- else if (publishToFloriday) {
920
- const payload = buildAddTradeItemPayload(data);
921
- const res = await floriday.createTradeItem(activeId, payload);
922
- if (!res?.attemptId) {
923
- throw new Error("Gateway gaf geen attemptId terug — aanmaken niet bevestigd");
924
- }
925
- const createdTradeItemId = res.tradeItemId ?? payload.tradeItemId;
926
- await saveWarehousePreference(createdTradeItemId, data.warehouseId);
927
- // Als dit een lokaal artikel was, ruim de draft-rij op
928
- if (draftId) {
929
- try {
930
- 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");
931
1223
  }
932
- catch {
933
- // Niet kritiek; rij verdwijnt na volgende reload
934
- }
935
- }
936
- // ---- Direct-sale flow -----------------------------------------
937
- // Alleen doorzetten als de gebruiker de "Aanbieden"-stap geactiveerd
938
- // heeft. Falen mag de TradeItem-succes NIET terugdraaien — daarom
939
- // in een aparte try/catch en apart toast bericht.
940
- if (data.offerForSale) {
941
- const tradeItemId = createdTradeItemId;
942
- // 1) Seed lokale voorraad (physical_stock) — de continuous-stock
943
- // trigger pusht dit naar Floriday zodra de trade item in de
944
- // cache verschijnt.
945
- // GUARD: nooit een BESTAANDE voorraad-rij overschrijven. Als
946
- // er al een rij is (bv. bij herpubliceren of bij een lokaal
947
- // product met historische voorraad), laten we die met rust
948
- // om ongewild op 0 zetten te voorkomen. Voorraad wordt
949
- // beheerd via de Voorraadkaart, niet via de wizard.
950
- try {
951
- const key = stockKeyFor({ tradeItemId });
952
- if (key && data.saleStockPieces > 0) {
953
- const { data: existing } = await supabase
954
- .from("trade_item_stock")
955
- .select("trade_item_key")
956
- .eq("connection_id", activeId)
957
- .eq("trade_item_key", key)
958
- .maybeSingle();
959
- if (!existing) {
960
- const userRes = await supabase.auth.getUser();
961
- const userId = userRes.data.user?.id;
962
- if (userId) {
963
- await supabase
964
- .from("trade_item_stock")
965
- .insert({
966
- user_id: userId,
967
- connection_id: activeId,
968
- trade_item_key: key,
969
- physical_stock: data.saleStockPieces,
970
- minimum_stock: 0,
971
- reserved_stock: 0,
972
- incoming_stock: 0,
973
- });
974
- }
975
- }
976
- }
977
- }
978
- catch (e) {
979
- toast.error(`Voorraad opslaan mislukt: ${e.message}`);
980
- }
981
- // 2) CustomerOffer publiceren (POST /customer-offers)
982
- try {
983
- const packingBase = buildPrimaryPackingBase(data);
984
- await publishCustomerOfferFn({
985
- data: {
986
- customerOfferId: crypto.randomUUID(),
987
- tradeItemId,
988
- title: data.name || data.vbnName || "Aanbod",
989
- description: "",
990
- pricePerPiece: data.salePricePerPiece,
991
- currency: "EUR",
992
- despatchWarehouseId: data.warehouseId,
993
- numberOfPieces: data.saleStockPieces,
994
- allowedCustomerOrganizationIds: data.customerOrganizationIds,
995
- packingConfiguration: packingBase,
996
- orderStart: new Date(`${data.offerStartDate}T00:00:00Z`).toISOString(),
997
- },
998
- });
999
- toast.success("Artikel aangemaakt en aangeboden in Floriday");
1000
- }
1001
- catch (e) {
1002
- toast.error(`Aanbod publiceren mislukt: ${e.message}`);
1003
- }
1004
- // 3) Supply-line pushen naar de Authenticatie-module gateway
1005
- // zodat het product met prijs online komt in de Floriday-catalogus.
1006
- // Idempotent op supplyLineId — herprijzen hergebruikt dezelfde regel.
1224
+ // Lokale kopie pas bijwerken nadat de handelsomgeving bevestigd heeft.
1007
1225
  try {
1008
- const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
1009
- data.packingConfigurations[0];
1010
- if (primary?.id && data.salePricePerPiece > 0) {
1011
- const sl = await upsertSupplyLineForTradeItem({
1012
- connectionId: activeId,
1013
- tradeItemId,
1014
- pricePerPiece: data.salePricePerPiece,
1015
- packingConfigurationId: primary.id,
1016
- availableQuantity: data.saleStockPieces,
1017
- customerOrganizationIds: data.customerOrganizationIds,
1018
- periodStart: data.offerStartDate
1019
- ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
1020
- : undefined,
1021
- });
1022
- if (sl.supplyLineId) {
1023
- toast.success("Prijs online gezet in Floriday-catalogus");
1024
- }
1025
- }
1026
- }
1027
- catch (e) {
1028
- 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);
1029
1234
  }
1030
- // Basisprijs + prijzen per prijsgroep spiegelen + pushen.
1031
- try {
1032
- const primary = data.packingConfigurations.find((p) => p.isPrimary) ??
1033
- data.packingConfigurations[0];
1034
- const priceSync = await syncTradeItemPrices({
1035
- tradeItemId,
1036
- tradeItemKey: tradeItemId,
1037
- availableQuantity: data.saleStockPieces,
1038
- currency: "EUR",
1039
- periodStart: data.offerStartDate
1040
- ? new Date(`${data.offerStartDate}T00:00:00Z`).toISOString()
1041
- : undefined,
1042
- basePricePerPiece: data.salePricePerPiece,
1043
- pricesByGroupId: data.priceGroupPrices ?? {},
1044
- priceGroups,
1045
- packingConfigurationId: primary?.id,
1046
- });
1047
- const failed = priceSync.groupResults.filter((r) => !r.ok);
1048
- if (failed.length) {
1049
- toast.error(`Prijsgroep(en) niet gepusht: ${failed.map((r) => r.name).join(", ")}`);
1050
- }
1235
+ catch {
1236
+ /* wordt automatisch bijgewerkt bij de volgende synchronisatie */
1051
1237
  }
1052
- catch (e) {
1053
- 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");
1054
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
+ });
1055
1249
  }
1056
- else {
1057
- toast.success("Artikel aangemaakt in Floriday");
1058
- }
1059
- }
1060
- else {
1061
- const newId = await upsertLocalTradeItem({
1062
- id: draftId,
1063
- connectionId: activeId,
1064
- data,
1065
- });
1066
- setDraftId(newId);
1067
- toast.success(draftId
1068
- ? "Wijzigingen opgeslagen (nog niet gepubliceerd)"
1069
- : "Artikel opgeslagen (nog niet gepubliceerd)");
1070
1250
  }
1071
- navigate({ to: "/voorraad" });
1072
- }
1073
- catch (e) {
1074
- const msg = e.message;
1075
- const missingCodes = isMissingCharacteristicsError(msg);
1076
- if (missingCodes.length > 0) {
1077
- // Floriday's server-side check gaf terug welke karakteristieken nog
1078
- // ontbreken. Schrijf ze naar de gedeelde Supabase-cache zodat iedere
1079
- // gebruiker deze eisen vanaf nu vóór de call te zien krijgt, en
1080
- // navigeer terug naar Eigenschappen zodat ze meteen invulbaar zijn.
1081
- if (data.vbnCode) {
1082
- try {
1083
- const res = await saveFloridayVbnRequired({
1084
- data: { vbnCode: data.vbnCode, codes: missingCodes },
1085
- });
1086
- queryClient.setQueryData(["floriday-vbn-required", data.vbnCode], { vbnCode: res.vbnCode, requiredCodes: res.requiredCodes });
1087
- }
1088
- 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(() => { });
1089
1261
  }
1090
- const eigenschappenIdx = visibleSteps(data).findIndex((s) => s.key === "eigenschappen");
1091
- if (eigenschappenIdx >= 0)
1092
- setStep(eigenschappenIdx);
1093
- 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" });
1094
1265
  }
1095
- else if (msg.includes("NO_ACTIVE_FLORIDAY_CONNECTION")) {
1096
- 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
+ });
1097
1270
  }
1098
1271
  else {
1099
- toast.error(`Opslaan mislukt: ${msg}`);
1272
+ toast.error("Publiceren is niet gelukt");
1100
1273
  }
1101
1274
  }
1275
+ catch (e) {
1276
+ const friendly = translatePublishError(e, { characteristicLabels });
1277
+ await handleStepFailure(friendly);
1278
+ if (jobId)
1279
+ await finishPublishJob(jobId);
1280
+ }
1102
1281
  finally {
1103
1282
  setSubmitting(false);
1104
1283
  }
@@ -1151,12 +1330,113 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1151
1330
  setSubmitting(false);
1152
1331
  }
1153
1332
  };
1154
- // Auto-save: zodra er een bestaand concept OF een gepubliceerd artikel in
1155
- // bewerking is, persisteren we wijzigingen debounced. Voor concepten gaan de
1156
- // wijzigingen naar `trade_item_drafts`; voor gepubliceerde artikelen wordt
1157
- // een PUT /trade-items/{id} naar Floriday gestuurd en de lokale cache
1158
- // meteen bijgewerkt, zodat aanpassingen (o.a. beladingen) direct overal in
1159
- // 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
+ ]);
1160
1440
  useEffect(() => {
1161
1441
  if (!activeId)
1162
1442
  return;
@@ -1175,66 +1455,12 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1175
1455
  }
1176
1456
  if (lastSavedRef.current === serialized)
1177
1457
  return;
1178
- const t = window.setTimeout(async () => {
1179
- setSyncState("saving");
1180
- setSyncError(null);
1181
- try {
1182
- // Altijd lokaal opslaan als we een draft-rij hebben
1183
- if (draftId) {
1184
- await upsertLocalTradeItem({ id: draftId, connectionId: activeId, data });
1185
- }
1186
- if (pushTargetId && !data.productGroup) {
1187
- setSyncState(draftId ? "local" : "idle");
1188
- lastSavedRef.current = serialized;
1189
- return;
1190
- }
1191
- // Push naar Floriday als er een gekoppeld/gepubliceerd trade-item is
1192
- if (pushTargetId) {
1193
- // Geen warehouse gekozen? Sla stil lokaal op — geen toast, geen rode
1194
- // pill. Zodra de gebruiker een warehouse kiest gaat de sync verder.
1195
- const whId = (data.warehouseId ?? "").trim();
1196
- 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);
1197
- if (!isUuid) {
1198
- setSyncState(draftId ? "local" : "idle");
1199
- lastSavedRef.current = serialized;
1200
- return;
1201
- }
1202
- await saveWarehousePreference(pushTargetId, data.warehouseId);
1203
- const payload = buildUpdateTradeItemPayload(data);
1204
- const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
1205
- if (!res?.attemptId) {
1206
- throw new Error("Gateway gaf geen attemptId terug");
1207
- }
1208
- try {
1209
- const cachePayload = {
1210
- ...buildAddTradeItemPayload(data),
1211
- tradeItemId: pushTargetId,
1212
- };
1213
- await supabase
1214
- .from("floriday_trade_items_cache")
1215
- .update({
1216
- data: cachePayload,
1217
- fetched_at: new Date().toISOString(),
1218
- })
1219
- .eq("connection_id", activeId)
1220
- .eq("floriday_id", pushTargetId);
1221
- }
1222
- catch {
1223
- /* niet kritiek; sync corrigeert */
1224
- }
1225
- setSyncState("saved");
1226
- }
1227
- else {
1228
- setSyncState("local");
1229
- }
1230
- lastSavedRef.current = serialized;
1231
- }
1232
- catch (e) {
1233
- const msg = e.message || "Onbekende fout";
1234
- setSyncError(msg);
1235
- setSyncState("error");
1236
- toast.error(`Sync naar Floriday mislukt: ${msg}`);
1237
- }
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);
1238
1464
  }, 800);
1239
1465
  return () => window.clearTimeout(t);
1240
1466
  }, [
@@ -1246,9 +1472,22 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1246
1472
  publishedLoading,
1247
1473
  productLoading,
1248
1474
  isEditingPublished,
1249
- saveWarehousePreference,
1250
1475
  tradeItemId,
1476
+ persistAndPush,
1251
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]);
1252
1491
  if (connLoading || draftLoading || publishedLoading || productLoading) {
1253
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
1254
1493
  ? "Artikel laden..."
@@ -1265,7 +1504,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1265
1504
  setDeleteOpen(o);
1266
1505
  if (!o)
1267
1506
  setDeleteAck(false);
1268
- }, 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: {
1269
1508
  name: data.name || data.vbnName,
1270
1509
  articleNumber: data.articleCode,
1271
1510
  gtin: data.gtin,
@@ -1293,7 +1532,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1293
1532
  ? "border-destructive bg-destructive text-destructive-foreground"
1294
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
1295
1534
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
1296
- : "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
1297
1536
  ? "Wijzigingen opslaan"
1298
1537
  : publishToFloriday
1299
1538
  ? isEditingDraft