@flowselections/floriday-voorraad 1.0.129 → 1.0.133

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 (47) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.js +102 -19
  3. package/dist-lib/components/voorraad/PriceStatusBadge.d.ts.map +1 -1
  4. package/dist-lib/components/voorraad/PriceStatusBadge.js +21 -45
  5. package/dist-lib/components/voorraad/ProductStructureCard.d.ts.map +1 -1
  6. package/dist-lib/components/voorraad/ProductStructureCard.js +6 -2
  7. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  8. package/dist-lib/hooks/useLocalTradeItems.js +4 -0
  9. package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
  10. package/dist-lib/hooks/useVoorraadData.js +2 -1
  11. package/dist-lib/integrations/supabase/types.d.ts +845 -0
  12. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  13. package/dist-lib/lib/__tests__/mirror-products.test.d.ts +2 -0
  14. package/dist-lib/lib/__tests__/mirror-products.test.d.ts.map +1 -0
  15. package/dist-lib/lib/__tests__/mirror-products.test.js +13 -0
  16. package/dist-lib/lib/__tests__/release-130.test.d.ts +2 -0
  17. package/dist-lib/lib/__tests__/release-130.test.d.ts.map +1 -0
  18. package/dist-lib/lib/__tests__/release-130.test.js +103 -0
  19. package/dist-lib/lib/floriday-2026v1.server.d.ts +1 -8
  20. package/dist-lib/lib/floriday-2026v1.server.d.ts.map +1 -1
  21. package/dist-lib/lib/floriday-2026v1.server.js +0 -10
  22. package/dist-lib/lib/floriday-gateway.functions.d.ts +6 -0
  23. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  24. package/dist-lib/lib/floriday-gateway.functions.js +7 -0
  25. package/dist-lib/lib/floriday-gateway.server.d.ts +17 -4
  26. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  27. package/dist-lib/lib/floriday-gateway.server.js +80 -2
  28. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  29. package/dist-lib/lib/floriday-payload.js +9 -4
  30. package/dist-lib/lib/floriday-read-contract.d.ts +49 -0
  31. package/dist-lib/lib/floriday-read-contract.d.ts.map +1 -0
  32. package/dist-lib/lib/floriday-read-contract.js +92 -0
  33. package/dist-lib/lib/mirror-products.d.ts +12 -0
  34. package/dist-lib/lib/mirror-products.d.ts.map +1 -0
  35. package/dist-lib/lib/mirror-products.js +10 -0
  36. package/dist-lib/lib/price-status.d.ts +14 -0
  37. package/dist-lib/lib/price-status.d.ts.map +1 -0
  38. package/dist-lib/lib/price-status.js +16 -0
  39. package/dist-lib/lib/publish-pipeline.d.ts.map +1 -1
  40. package/dist-lib/lib/publish-pipeline.js +2 -7
  41. package/dist-lib/lib/publish-status.d.ts +26 -0
  42. package/dist-lib/lib/publish-status.d.ts.map +1 -0
  43. package/dist-lib/lib/publish-status.js +30 -0
  44. package/dist-lib/server-hooks/floriday-continuous-stock-pull.d.ts.map +1 -1
  45. package/dist-lib/server-hooks/floriday-continuous-stock-pull.js +37 -50
  46. package/package.json +1 -1
  47. package/sql/floriday-voorraad-schema.sql +232 -53
@@ -1 +1 @@
1
- {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA2DA,OAAO,EAUL,KAAK,UAAU,EAMhB,MAAM,4BAA4B,CAAC;AAwDpC,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,UAAU,CA0BrF;AAiED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,GACR,GAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,aAAa,CAAC;CACpB,+BA4xEL"}
1
+ {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA2DA,OAAO,EAUL,KAAK,UAAU,EAMhB,MAAM,4BAA4B,CAAC;AA2DpC,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,UAAU,CA0BrF;AAiED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,GACR,GAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,aAAa,CAAC;CACpB,+BAo2EL"}
@@ -20,6 +20,9 @@ import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures.j
20
20
  import { useFloridayVbnRequired } from "../../hooks/useFloridayVbnRequired.js";
21
21
  import { saveFloridayVbnRequired } from "../../lib/floriday-vbn-required.functions.js";
22
22
  import { useQuery, useQueryClient } from "@tanstack/react-query";
23
+ import { fetchFloridayWriteMode } from "../../lib/floriday-write-mode.js";
24
+ import { editTargetText } from "../../lib/publish-status.js";
25
+ import { articleCodeProblem } from "../../lib/article-code.js";
23
26
  import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday.js";
24
27
  import { useFloridayPriceGroups } from "../../hooks/useFloridayPriceGroups.js";
25
28
  import { readTradeItemPrices, syncTradeItemPrices } from "../../lib/trade-item-prices.js";
@@ -178,7 +181,9 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
178
181
  const isEditingDraft = Boolean(initialDraftId);
179
182
  // Een lokaal product dat al aan Floriday gekoppeld is, is géén "nieuw"
180
183
  // artikel: bewerken betekent dan bijwerken in Floriday.
181
- const publishedTradeItemId = tradeItemId ?? productMeta.floridayTradeItemId ?? null;
184
+ // 1.0.130: een artikel met een Floriday-id telt als gepubliceerd, ook als
185
+ // alleen de lokale rij (draftFloridayId) dat id kent.
186
+ const publishedTradeItemId = tradeItemId ?? productMeta.floridayTradeItemId ?? draftFloridayId ?? null;
182
187
  const isEditingPublished = Boolean(publishedTradeItemId);
183
188
  const isEditingProduct = Boolean(productId);
184
189
  const isEditing = isEditingDraft || isEditingPublished || isEditingProduct;
@@ -422,29 +427,70 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
422
427
  }
423
428
  const cachePackings = cacheData?.packingConfigurations ?? [];
424
429
  const payloadHasPacking = Array.isArray(payload.packingConfigurations) && payload.packingConfigurations.length > 0;
430
+ // 1.0.130: zonder Floriday-kopie of opgeslagen invoer eerst de eigen
431
+ // hoofdverpakking (product_packings) gebruiken, inclusief fust en
432
+ // ladingdrager. Kent deze rij die niet, dan die van het artikel met
433
+ // hetzelfde Floriday-id. Nooit aanvullen.
434
+ let ownPrimary = null;
435
+ if (cachePackings.length === 0 && !payloadHasPacking) {
436
+ const pidList = [String(product.id)];
437
+ if (fid) {
438
+ const { data: sisters } = await supabase
439
+ .from("products")
440
+ .select("id")
441
+ .eq("floriday_trade_item_id", fid)
442
+ .limit(5);
443
+ for (const r of (sisters ?? []))
444
+ if (!pidList.includes(String(r.id)))
445
+ pidList.push(String(r.id));
446
+ }
447
+ const { data: pk } = await supabase
448
+ .from("product_packings")
449
+ .select("product_id, vbn_package_code, custom_package_id, load_carrier, pieces_per_package, packages_per_layer, layers_per_load_carrier")
450
+ .in("product_id", pidList)
451
+ .eq("is_primary", true);
452
+ const rows = (pk ?? []);
453
+ ownPrimary = rows.find((r) => r.product_id === pidList[0]) ?? rows[0] ?? null;
454
+ }
425
455
  const seededPacking = cachePackings.length > 0
426
456
  ? cachePackings
427
457
  : payloadHasPacking
428
458
  ? payload.packingConfigurations
429
- : ppc > 0 || cpl > 0 || lpc > 0
459
+ : ownPrimary
430
460
  ? [
431
461
  {
432
- id: typeof crypto !== "undefined" && "randomUUID" in crypto
433
- ? crypto.randomUUID()
434
- : `pk-${Date.now()}`,
435
- source: "VBN",
436
- loadCarrier: "",
437
- packagingType: "",
438
- // Leeg laten wat niet bekend is; nooit aanvullen.
439
- unitsPerPackage: ppc,
440
- packagesPerLayer: cpl,
441
- layersPerLoadCarrier: lpc,
462
+ id: `own-${String(product.id)}`,
463
+ source: ownPrimary.custom_package_id ? "SUPPLIER" : "VBN",
464
+ loadCarrier: String(ownPrimary.load_carrier ?? ""),
465
+ packagingType: ownPrimary.vbn_package_code != null ? String(ownPrimary.vbn_package_code) : "",
466
+ packagingMaterialId: ownPrimary.custom_package_id ?? undefined,
467
+ unitsPerPackage: Number(ownPrimary.pieces_per_package) || 0,
468
+ packagesPerLayer: Number(ownPrimary.packages_per_layer) || 0,
469
+ layersPerLoadCarrier: Number(ownPrimary.layers_per_load_carrier) || 0,
442
470
  isPrimary: true,
443
471
  isCustomerSpecific: false,
444
472
  customerOrganizationIds: [],
445
473
  },
446
474
  ]
447
- : [];
475
+ : ppc > 0 || cpl > 0 || lpc > 0
476
+ ? [
477
+ {
478
+ id: typeof crypto !== "undefined" && "randomUUID" in crypto
479
+ ? crypto.randomUUID()
480
+ : `pk-${Date.now()}`,
481
+ source: "VBN",
482
+ loadCarrier: "",
483
+ packagingType: "",
484
+ // Leeg laten wat niet bekend is; nooit aanvullen.
485
+ unitsPerPackage: ppc,
486
+ packagesPerLayer: cpl,
487
+ layersPerLoadCarrier: lpc,
488
+ isPrimary: true,
489
+ isCustomerSpecific: false,
490
+ customerOrganizationIds: [],
491
+ },
492
+ ]
493
+ : [];
448
494
  const cacheImages = cacheData?.images ?? [];
449
495
  const mergedImages = cacheImages.length > 0
450
496
  ? cacheImages
@@ -969,8 +1015,21 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
969
1015
  out[c.code] = c.label || c.code;
970
1016
  return out;
971
1017
  }, [spec.characteristics]);
972
- const wantsPublish = (isEditingPublished && Boolean(tradeItemId)) ||
973
- (!isEditingPublished && publishToFloriday);
1018
+ // Met een Floriday-id gaan wijzigingen altijd naar Floriday.
1019
+ const wantsPublish = isEditingPublished || publishToFloriday;
1020
+ const writeModeQuery = useQuery({
1021
+ queryKey: ["floriday-write-mode"],
1022
+ queryFn: fetchFloridayWriteMode,
1023
+ enabled: isEditingPublished,
1024
+ staleTime: 30000,
1025
+ });
1026
+ const editTarget = editTargetText({
1027
+ floridayId: publishedTradeItemId,
1028
+ writeReadOnly: writeModeQuery.isError ? true : writeModeQuery.data?.read_only,
1029
+ writeReason: writeModeQuery.isError
1030
+ ? "de Floriday-status kon niet worden gelezen"
1031
+ : writeModeQuery.data?.reason,
1032
+ });
974
1033
  const readiness = useMemo(() => {
975
1034
  const r = buildReadinessReport({
976
1035
  data,
@@ -1393,6 +1452,18 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1393
1452
  resolvedTradeItemId =
1394
1453
  core.value?.tradeItemId ??
1395
1454
  resolvedTradeItemId;
1455
+ // Artikel staat in Floriday: vanaf nu gepubliceerd, ook als een latere
1456
+ // stap (aanbod, controle) mislukt.
1457
+ if (resolvedTradeItemId) {
1458
+ setDraftFloridayId(resolvedTradeItemId);
1459
+ if (savedLocalId) {
1460
+ await markLocalSyncStatus(savedLocalId, {
1461
+ dirty: false,
1462
+ last_error: null,
1463
+ last_synced_at: new Date().toISOString(),
1464
+ }).catch(() => { });
1465
+ }
1466
+ }
1396
1467
  // Nieuw artikel: ingevoerde voorraad, prijs en belading altijd lokaal vastleggen
1397
1468
  // (nooit iets overschrijven wat er al staat).
1398
1469
  if (!publishedTradeItemId) {
@@ -1564,8 +1635,8 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1564
1635
  return { warnings };
1565
1636
  }, stepOpts);
1566
1637
  if (!verify.ok) {
1567
- toast.warning("Het product is opgeslagen, maar nog niet bevestigd", {
1568
- description: "Controleer het later bij Publicaties.",
1638
+ toast.warning("Gepubliceerd; de controle achteraf lukte niet", {
1639
+ description: `${verify.error?.message ?? "Onbekende reden"} — het artikel staat wel in Floriday.`,
1569
1640
  });
1570
1641
  }
1571
1642
  }
@@ -1587,6 +1658,14 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1587
1658
  navigate({ to: "/voorraad" });
1588
1659
  }
1589
1660
  else if (status === "succeeded_with_warnings") {
1661
+ if (draftId) {
1662
+ await markLocalSyncStatus(draftId, {
1663
+ dirty: false,
1664
+ last_error: null,
1665
+ last_synced_at: new Date().toISOString(),
1666
+ }).catch(() => { });
1667
+ }
1668
+ setPendingSync(null);
1590
1669
  toast.warning("Gepubliceerd, met aandachtspunten", {
1591
1670
  description: "Bekijk wat er nog moet gebeuren in het overzicht hieronder.",
1592
1671
  });
@@ -1820,7 +1899,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1820
1899
  setDeleteOpen(o);
1821
1900
  if (!o)
1822
1901
  setDeleteAck(false);
1823
- }, 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, productId: productId ?? 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(FloridayStockReasonLine, { productId: linkedProductId ?? productId ?? null }), _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(ProductStructureCard, { productId: linkedProductId ?? productId ?? null }), _jsx(ArtikelCompositionCard, { productId: linkedProductId ?? productId ?? null, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1902
+ }, 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, productId: productId ?? 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(FloridayStockReasonLine, { productId: linkedProductId ?? productId ?? null }), _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" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [editTarget.text, " 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(ProductStructureCard, { productId: linkedProductId ?? productId ?? null }), _jsx(ArtikelCompositionCard, { productId: linkedProductId ?? productId ?? null, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1824
1903
  name: data.name || data.vbnName,
1825
1904
  articleNumber: data.articleCode,
1826
1905
  gtin: data.gtin,
@@ -2161,7 +2240,11 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList, articl
2161
2240
  icon: ClipboardList,
2162
2241
  },
2163
2242
  ];
2164
- return (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Artikelcode ", !articleCodeLocked && _jsx("span", { className: "text-destructive", children: "*" }), _jsx(InfoTip, { text: "Dit is je eigen referentie naar dit artikel, ook wel bekend als de kweker-artikelcode." })] }), _jsx(Input, { value: data.articleCode, onChange: (e) => update("articleCode", e.target.value), disabled: articleCodeLocked, readOnly: articleCodeLocked }), articleCodeLocked && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Artikelcode ligt vast in Floriday en kan niet meer gewijzigd worden." }))] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Naam ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { value: data.name, onChange: (e) => update("name", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["GTIN ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" }), _jsx(InfoTip, { text: "GTIN staat voor 'Global Trade Item Number', een 13-cijferige barcode" })] }), _jsx(Input, { value: data.gtin, onChange: (e) => update("gtin", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Collecties ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" })] }), data.collections.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: data.collections.map((c) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-1 text-xs", children: [c, _jsx("button", { type: "button", onClick: () => removeCollection(c), className: "hover:text-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, c))) })), !showNewCollection ? (_jsxs(Select, { value: "", onValueChange: (v) => {
2243
+ return (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Artikelcode ", !articleCodeLocked && _jsx("span", { className: "text-destructive", children: "*" }), _jsx(InfoTip, { text: "Dit is je eigen referentie naar dit artikel, ook wel bekend als de kweker-artikelcode." })] }), _jsx(Input, { value: data.articleCode, onChange: (e) => {
2244
+ // Een eerdere opslagfout over de code verdwijnt zodra de code wijzigt.
2245
+ toast.dismiss();
2246
+ update("articleCode", e.target.value);
2247
+ }, disabled: articleCodeLocked, readOnly: articleCodeLocked }), !articleCodeLocked && data.articleCode.trim() && articleCodeProblem(data.articleCode, data.name, "manual") && (_jsxs("p", { className: "text-xs text-destructive", children: [articleCodeProblem(data.articleCode, data.name, "manual"), " Vul de echte artikelcode in."] })), articleCodeLocked && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Artikelcode ligt vast in Floriday en kan niet meer gewijzigd worden." }))] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Naam ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { value: data.name, onChange: (e) => update("name", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["GTIN ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" }), _jsx(InfoTip, { text: "GTIN staat voor 'Global Trade Item Number', een 13-cijferige barcode" })] }), _jsx(Input, { value: data.gtin, onChange: (e) => update("gtin", e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { children: ["Collecties ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" })] }), data.collections.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: data.collections.map((c) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-1 text-xs", children: [c, _jsx("button", { type: "button", onClick: () => removeCollection(c), className: "hover:text-destructive", children: _jsx(X, { className: "h-3 w-3" }) })] }, c))) })), !showNewCollection ? (_jsxs(Select, { value: "", onValueChange: (v) => {
2165
2248
  if (v === "__new__") {
2166
2249
  setShowNewCollection(true);
2167
2250
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"PriceStatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/PriceStatusBadge.tsx"],"names":[],"mappings":"AA2FA,KAAK,KAAK,GAAG;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAeF,wBAAgB,gBAAgB,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,KAAK,sCAmG/E"}
1
+ {"version":3,"file":"PriceStatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/PriceStatusBadge.tsx"],"names":[],"mappings":"AA0FA,KAAK,KAAK,GAAG;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAeF,wBAAgB,gBAAgB,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,KAAK,sCAsD/E"}
@@ -4,6 +4,7 @@ import { CircleDollarSign, Clock, MinusCircle, Loader2 } from "lucide-react";
4
4
  import { useEffect, useState } from "react";
5
5
  import { useTradeItemSupplyLine } from "../../hooks/useTradeItemSupplyLine.js";
6
6
  import { supabase } from "../../integrations/supabase/client.js";
7
+ import { PRICE_STATUS_LABELS, priceStatus } from "../../lib/price-status.js";
7
8
  /**
8
9
  * M26: laatst naar Floriday verzonden weekprijs (weekaanbod), uit de wachtrij.
9
10
  * M28: dezelfde bron als de verzendgeschiedenis ("Laatst naar Floriday"):
@@ -56,12 +57,10 @@ function useLastSentPrice(tradeItemId, productId) {
56
57
  function useResolvedTradeItemId(tradeItemId, productId) {
57
58
  const [resolved, setResolved] = useState(tradeItemId ?? null);
58
59
  useEffect(() => {
59
- if (tradeItemId) {
60
- setResolved(tradeItemId);
61
- return;
62
- }
60
+ // 1.0.130: met een artikel-id is products.floriday_trade_item_id leidend,
61
+ // zodat elke plek op hetzelfde artikel dezelfde bron leest.
63
62
  if (!productId) {
64
- setResolved(null);
63
+ setResolved(tradeItemId ?? null);
65
64
  return;
66
65
  }
67
66
  let cancelled = false;
@@ -72,7 +71,7 @@ function useResolvedTradeItemId(tradeItemId, productId) {
72
71
  .maybeSingle()
73
72
  .then(({ data }) => {
74
73
  if (!cancelled)
75
- setResolved(data?.floriday_trade_item_id ?? null);
74
+ setResolved(data?.floriday_trade_item_id ?? tradeItemId ?? null);
76
75
  });
77
76
  return () => {
78
77
  cancelled = true;
@@ -103,44 +102,20 @@ export function PriceStatusBadge({ tradeItemId, productId, offerForSale }) {
103
102
  if (isLoading) {
104
103
  return (_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" }), " Prijsstatus laden\u2026"] }));
105
104
  }
106
- // "Te koop aanbieden" staat uit: nooit "Prijs live" tonen. Wel de laatst
107
- // verzonden prijs melden als die er is (zelfde bron als de geschiedenis).
108
- if (!offerForSale) {
109
- if (sent) {
110
- return (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full cursor-help bg-amber-100 text-amber-800", title: `Laatst verzonden op ${new Date(sent.at).toLocaleString("nl-NL")} · week ${sent.week ?? "?"}.\n"Te koop aanbieden" staat uit: dit artikel is niet te koop in Floriday.`, children: [_jsx(CircleDollarSign, { className: "h-3 w-3" }), " Laatst verzonden: \u20AC ", sent.price.toFixed(2), " \u2013 niet te koop in Floriday"] }));
111
- }
112
- return (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full cursor-help bg-muted text-muted-foreground", title: '"Te koop aanbieden" staat uit.', children: [_jsx(MinusCircle, { className: "h-3 w-3" }), " Geen prijs actief"] }));
113
- }
114
- if (!supplyLine && sent) {
115
- return (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full cursor-help bg-emerald-100 text-emerald-700", title: `Weekaanbod verzonden op ${new Date(sent.at).toLocaleString("nl-NL")}`, children: [_jsx(CircleDollarSign, { className: "h-3 w-3" }), " Prijs live in Floriday \u00B7 week ", sent.week ?? "?", " \u00B7 \u20AC ", sent.price.toFixed(2)] }));
116
- }
117
- const now = Date.now();
118
- const endMs = supplyLine?.period_end ? new Date(supplyLine.period_end).getTime() : null;
105
+ // Eén bron, één label: de prijsregel in de Floriday-catalogus.
106
+ const variant = priceStatus(supplyLine ?? null);
107
+ const label = PRICE_STATUS_LABELS[variant];
108
+ const Icon = variant === "live" ? CircleDollarSign : variant === "none" ? MinusCircle : Clock;
109
+ const cls = variant === "live"
110
+ ? "bg-emerald-100 text-emerald-700"
111
+ : variant === "none"
112
+ ? "bg-muted text-muted-foreground"
113
+ : "bg-amber-100 text-amber-800";
119
114
  const qty = supplyLine?.available_quantity ?? 0;
120
- const isExpired = endMs !== null && endMs < now;
121
- const isDrained = supplyLine !== null && qty <= 0;
122
- let label;
123
- let Icon = CircleDollarSign;
124
- let cls = "";
125
- let variant;
126
- if (!supplyLine) {
127
- variant = "none";
128
- label = "Geen prijs actief";
129
- Icon = MinusCircle;
130
- cls = "bg-muted text-muted-foreground";
131
- }
132
- else if (isExpired || isDrained) {
133
- variant = "expired";
134
- label = isExpired ? "Prijs verlopen" : "Voorraad op";
135
- Icon = Clock;
136
- cls = "bg-amber-100 text-amber-800";
137
- }
138
- else {
139
- variant = "live";
140
- label = "Prijs live in catalogus";
141
- Icon = CircleDollarSign;
142
- cls = "bg-emerald-100 text-emerald-700";
143
- }
115
+ const extra = [
116
+ !offerForSale ? '"Te koop aanbieden" staat uit.' : null,
117
+ sent ? `Laatst verzonden weekaanbod: € ${sent.price.toFixed(2)} · week ${sent.week ?? "?"} (${new Date(sent.at).toLocaleString("nl-NL")}).` : null,
118
+ ].filter(Boolean);
144
119
  const title = supplyLine
145
120
  ? [
146
121
  `Prijs: ${supplyLine.price_per_piece != null
@@ -151,6 +126,7 @@ export function PriceStatusBadge({ tradeItemId, productId, offerForSale }) {
151
126
  ]
152
127
  .filter(Boolean)
153
128
  .join("\n")
154
- : "Nog geen prijs naar Floriday-catalogus gepusht.";
155
- return (_jsxs("span", { className: `inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full cursor-help ${cls}`, title: title, children: [_jsx(Icon, { className: "h-3 w-3" }), " ", label] }));
129
+ : "Nog geen prijs in de Floriday-catalogus.";
130
+ const fullTitle = [title, ...extra].join("\n");
131
+ return (_jsxs("span", { className: `inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full cursor-help ${cls}`, title: fullTitle, children: [_jsx(Icon, { className: "h-3 w-3" }), " ", label] }));
156
132
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProductStructureCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductStructureCard.tsx"],"names":[],"mappings":"AAiDA,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,sCAoQ/E;AAED,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,sCAkClF"}
1
+ {"version":3,"file":"ProductStructureCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductStructureCard.tsx"],"names":[],"mappings":"AAkDA,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,sCAuQ/E;AAED,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,sCAkClF"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { isPublishedToFloriday } from "../../lib/publish-status.js";
2
3
  /**
3
4
  * Overzicht per producttype op de artikelpagina. Alle totalen komen uit de
4
5
  * gedeelde databasefunctie `get_product_structure_totals`; hier wordt niets
@@ -16,6 +17,7 @@ export function ProductStructureCard({ productId }) {
16
17
  const [totals, setTotals] = useState(null);
17
18
  const [setting, setSetting] = useState(null);
18
19
  const [queue, setQueue] = useState(null);
20
+ const [floridayId, setFloridayId] = useState(null);
19
21
  const [names, setNames] = useState(new Map());
20
22
  const [groupHeads, setGroupHeads] = useState([]);
21
23
  const [error, setError] = useState(null);
@@ -25,7 +27,7 @@ export function ProductStructureCard({ productId }) {
25
27
  let cancelled = false;
26
28
  (async () => {
27
29
  setLoading(true);
28
- const [tRes, sRes, qRes] = await Promise.all([
30
+ const [tRes, sRes, qRes, pRes] = await Promise.all([
29
31
  supabase.rpc("get_product_structure_totals", { p_product_id: productId }),
30
32
  supabase
31
33
  .from("floriday_inventory_settings")
@@ -41,6 +43,7 @@ export function ProductStructureCard({ productId }) {
41
43
  .order("created_at", { ascending: false })
42
44
  .limit(1)
43
45
  .maybeSingle(),
46
+ supabase.from("products").select("floriday_trade_item_id").eq("id", productId).maybeSingle(),
44
47
  ]);
45
48
  if (cancelled)
46
49
  return;
@@ -53,6 +56,7 @@ export function ProductStructureCard({ productId }) {
53
56
  setTotals(t);
54
57
  setSetting(sRes.data ?? null);
55
58
  setQueue(qRes.data ?? null);
59
+ setFloridayId(pRes.data?.floriday_trade_item_id ?? null);
56
60
  const ids = new Set();
57
61
  t?.parts?.forEach((p) => p.product_id && ids.add(p.product_id));
58
62
  t?.component_of_sets?.forEach((id) => ids.add(id));
@@ -104,7 +108,7 @@ export function ProductStructureCard({ productId }) {
104
108
  return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(CardTitle, { children: "Producttype" }), _jsx(Badge, { className: PRODUCT_TYPE_CLASSES[type], title: PRODUCT_TYPE_HELP[type], children: PRODUCT_TYPE_LABELS[type] }), _jsx("span", { title: PRODUCT_TYPE_HELP[type], "aria-label": PRODUCT_TYPE_HELP[type], children: _jsx(Info, { className: "h-4 w-4 text-muted-foreground" }) })] }), _jsx(CardDescription, { children: PRODUCT_TYPE_HELP[type] })] }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [structureReason && (_jsxs("div", { className: "rounded border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive", children: ["Actie nodig: ", structureReason] })), type === "GROUP" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "font-medium", children: [publishedLabel(desired, setting?.packing_size), " \u00B7 som van ", parts.length, " ", parts.length === 1 ? "lid" : "leden", " (", totals?.sum_members ?? 0, " beschikbaar)"] }), _jsx("p", { className: "text-muted-foreground", children: "De leden staan niet los in Floriday; alleen deze groep wordt gepubliceerd. Volgorde = voorraadprioriteit (te wijzigen in de kaart Productgroep hieronder)." }), _jsxs("details", { className: "rounded border", children: [_jsxs("summary", { className: "cursor-pointer px-3 py-2", children: ["Leden tonen (", parts.length, ")"] }), _jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { className: "text-left text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "p-2", children: "#" }), _jsx("th", { className: "p-2", children: "Lid" }), _jsx("th", { className: "p-2 text-right", children: "Voorraad" }), _jsx("th", { className: "p-2 text-right", children: "Gereserveerd" }), _jsx("th", { className: "p-2 text-right", children: "Beschikbaar" })] }) }), _jsx("tbody", { children: parts.map((p, i) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "p-2", children: i + 1 }), _jsxs("td", { className: "p-2", children: [p.product_id ? (_jsx(Link, { to: "/voorraad/$id", params: { id: p.product_id }, className: "underline", children: name(p.product_id) })) : ("—"), p.reason && _jsxs("span", { className: "ml-1 text-destructive", children: ["(", ACTION_REASON_LABELS[p.reason] ?? p.reason, ")"] })] }), _jsx("td", { className: "p-2 text-right tabular-nums", children: p.physical ?? 0 }), _jsx("td", { className: "p-2 text-right tabular-nums", children: p.reserved ?? 0 }), _jsx("td", { className: "p-2 text-right tabular-nums", children: p.available ?? 0 })] }, p.product_id ?? i))) })] })] })] })), type === "SET" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "font-medium", children: ["Berekend: ", totals?.computed ?? 0, " sets", totals?.limiting_product_id && (_jsxs(_Fragment, { children: [" ", "\u00B7 beperkt door: ", name(totals.limiting_product_id), ", ", totals?.computed ?? 0, " sets"] }))] }), _jsx("div", { className: "rounded border border-amber-300 bg-amber-50 px-3 py-2 text-amber-900", children: "Deze set gaat nog niet naar Floriday (\"geen aanbod\") totdat de Logistieke engine bij een bestelling de onderdelen reserveert en afboekt." }), _jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { className: "text-left text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "p-2", children: "Onderdeel" }), _jsx("th", { className: "p-2 text-right", children: "Per set" }), _jsx("th", { className: "p-2 text-right", children: "Beschikbaar" }), _jsx("th", { className: "p-2 text-right", children: "Genoeg voor" })] }) }), _jsx("tbody", { children: parts.map((p, i) => {
105
109
  const limiting = p.product_id && p.product_id === totals?.limiting_product_id;
106
110
  return (_jsxs("tr", { className: `border-t ${limiting ? "font-medium" : ""}`, children: [_jsxs("td", { className: "p-2", children: [p.product_id ? (_jsx(Link, { to: "/voorraad/$id", params: { id: p.product_id }, className: "underline", children: name(p.product_id) })) : ("—"), limiting && _jsx("span", { className: "ml-1 text-amber-700", children: "(bepaalt het maximum)" })] }), _jsx("td", { className: "p-2 text-right tabular-nums", children: p.quantity ?? 1 }), _jsx("td", { className: "p-2 text-right tabular-nums", children: p.available ?? 0 }), _jsxs("td", { className: "p-2 text-right tabular-nums", children: [p.possible ?? 0, " sets"] })] }, (p.product_id ?? "") + i));
107
- }) })] }), _jsx("p", { className: "text-muted-foreground", children: "De onderdelen blijven ook los te koop in Floriday." })] })), groupHeads.length > 0 && (_jsx("div", { className: "rounded border px-3 py-2", children: groupHeads.map((g) => (_jsxs("div", { children: ["Lid van groep", " ", g.headId ? (_jsx(Link, { to: "/voorraad/$id", params: { id: g.headId }, className: "underline font-medium", children: g.name })) : (_jsx("span", { className: "font-medium", children: g.name })), " ", "\u2014 staat niet los in Floriday; de groep wordt gepubliceerd."] }, g.id))) })), (totals?.component_of_sets?.length ?? 0) > 0 && (_jsxs("div", { className: "rounded border px-3 py-2", children: ["Onderdeel van:", " ", totals.component_of_sets.map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx(Link, { to: "/voorraad/$id", params: { id }, className: "underline", children: name(id) })] }, id))), ". Een verkoop van dit artikel verlaagt ook de berekende stand van die sets."] })), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Floriday: ", reason ?? (queue?.status === "sent" ? "doorgegeven" : "Nog niet gepubliceerd")] })] })] }));
111
+ }) })] }), _jsx("p", { className: "text-muted-foreground", children: "De onderdelen blijven ook los te koop in Floriday." })] })), groupHeads.length > 0 && (_jsx("div", { className: "rounded border px-3 py-2", children: groupHeads.map((g) => (_jsxs("div", { children: ["Lid van groep", " ", g.headId ? (_jsx(Link, { to: "/voorraad/$id", params: { id: g.headId }, className: "underline font-medium", children: g.name })) : (_jsx("span", { className: "font-medium", children: g.name })), " ", "\u2014 staat niet los in Floriday; de groep wordt gepubliceerd."] }, g.id))) })), (totals?.component_of_sets?.length ?? 0) > 0 && (_jsxs("div", { className: "rounded border px-3 py-2", children: ["Onderdeel van:", " ", totals.component_of_sets.map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx(Link, { to: "/voorraad/$id", params: { id }, className: "underline", children: name(id) })] }, id))), ". Een verkoop van dit artikel verlaagt ook de berekende stand van die sets."] })), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Floriday: ", reason ?? (queue?.status === "sent" ? "doorgegeven" : isPublishedToFloriday(floridayId) ? "gepubliceerd" : "Nog niet gepubliceerd")] })] })] }));
108
112
  }
109
113
  /** Eén regel met de echte reden waarom de voorraad (nog) niet in Floriday staat. */
110
114
  export function FloridayStockReasonLine({ productId }) {
@@ -1 +1 @@
1
- {"version":3,"file":"useLocalTradeItems.d.ts","sourceRoot":"","sources":["../../src/hooks/useLocalTradeItems.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAwB,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAIvG;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;IAC1C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC1C,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AA0ED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;;;;EA0B9D;AAED,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA4BrF;AAED,MAAM,WAAW,SAAS;IACxB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC;CACpC;AAED,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAWjF;AAqBD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuGlB;AAED,wEAAwE;AACxE,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAGD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhF;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGpE"}
1
+ {"version":3,"file":"useLocalTradeItems.d.ts","sourceRoot":"","sources":["../../src/hooks/useLocalTradeItems.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAwB,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAIvG;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;IAC1C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC1C,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AA0ED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;;;;EA0B9D;AAED,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA4BrF;AAED,MAAM,WAAW,SAAS;IACxB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC;CACpC;AAED,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAWjF;AAqBD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0GlB;AAED,wEAAwE;AACxE,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAGD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhF;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGpE"}
@@ -188,6 +188,9 @@ export async function upsertLocalTradeItem(params) {
188
188
  }
189
189
  if (payload.name)
190
190
  productPatch.product = payload.name;
191
+ // Eén prijsbron: de basisprijs uit het artikelscherm is products.sale_price.
192
+ if (Number(payload.salePricePerPiece) > 0)
193
+ productPatch.sale_price = Number(payload.salePricePerPiece);
191
194
  if (payload.articleCode)
192
195
  productPatch.article_number = payload.articleCode;
193
196
  if (payload.gtin)
@@ -228,6 +231,7 @@ export async function upsertLocalTradeItem(params) {
228
231
  batch: "-",
229
232
  location: "onbekend",
230
233
  quantity: 0,
234
+ ...(Number(payload.salePricePerPiece) > 0 ? { sale_price: Number(payload.salePricePerPiece) } : {}),
231
235
  unit: "stuks",
232
236
  article_number: payload.articleCode || null,
233
237
  barcode: payload.gtin || null,
@@ -1 +1 @@
1
- {"version":3,"file":"useVoorraadData.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoorraadData.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAsB5E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;;EA2U1D"}
1
+ {"version":3,"file":"useVoorraadData.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoorraadData.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAsB5E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;;EA2U1D"}
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { hideMirrorItems } from "../lib/mirror-products.js";
2
3
  import { createBundler, pageRequestGuard } from "../lib/request-guard.js";
3
4
  import { supabase } from "../integrations/supabase/client.js";
4
5
  import { normaliseTradeItem } from "../lib/floriday-client.js";
@@ -295,7 +296,7 @@ export function useVoorraadData(connectionId) {
295
296
  };
296
297
  // Stabiele identiteit: alleen een nieuwe lijst als de onderliggende data verandert.
297
298
  const merged = useMemo(() => [
298
- ...localItems.map((i) => applyProductGroupReservation(classify(attachFloriday(applyStock(i, lookupStock(stockMap, {
299
+ ...hideMirrorItems(localItems).map((i) => applyProductGroupReservation(classify(attachFloriday(applyStock(i, lookupStock(stockMap, {
299
300
  tradeItemKey: i.tradeItemKey ?? null,
300
301
  floridayId: i.floridayId ?? null,
301
302
  productId: i.productId ?? null,