@flowselections/floriday-voorraad 1.0.76 → 1.0.77

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.
@@ -1517,7 +1517,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1517
1517
  const vbnValid = validateStepLocal("vbn", data).ok;
1518
1518
  const saveDraft = async () => {
1519
1519
  if (!activeId) {
1520
- toast.error("Geen actieve Floriday-connectie geselecteerd");
1520
+ toast.error("Geen actieve Floriday-koppeling. Kijk bij Instellingen › Floriday.");
1521
1521
  return;
1522
1522
  }
1523
1523
  // Voor een nieuw concept blijft een VBN-code de minimale poort. Voor
@@ -1715,10 +1715,10 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1715
1715
  ? "Lokaal artikel laden..."
1716
1716
  : productLoading
1717
1717
  ? "Product laden..."
1718
- : "Floriday-connectie laden..."] }) }));
1718
+ : "Floriday-koppeling laden..."] }) }));
1719
1719
  }
1720
1720
  if (!activeConnection) {
1721
- return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-connectie gevonden. Voeg er eerst een toe in Instellingen." }) }));
1721
+ return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-koppeling gevonden. Voeg er eerst een toe in Instellingen \u203A Floriday." }) }));
1722
1722
  }
1723
1723
  return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/voorraad", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Terug naar voorraad"] }) }), !isEditingPublished && (_jsxs(Button, { variant: "outline", size: "sm", className: "ml-auto", onClick: saveDraft, disabled: submitting || (!draftId && !vbnValid), children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), "Product opslaan"] })), (isEditingDraft || isEditingProduct) && (_jsxs(AlertDialog, { open: deleteOpen, onOpenChange: (o) => {
1724
1724
  setDeleteOpen(o);
@@ -2152,10 +2152,10 @@ function StepEigenschappen({ data, update, spec, schemaQuery, regulatoryQuery })
2152
2152
  return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Selecteer eerst een VBN-code bij stap 1 om de bijbehorende eigenschappen te laden." }));
2153
2153
  }
2154
2154
  if (schemaQuery.isLoading) {
2155
- return (_jsxs("p", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Schema wordt geladen\u2026"] }));
2155
+ return (_jsxs("p", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Kenmerken uit Floriday laden\u2026"] }));
2156
2156
  }
2157
2157
  if (schemaQuery.isError || !schemaQuery.data) {
2158
- return (_jsxs("p", { className: "text-sm text-destructive", children: ["Kon het Floriday trade-item schema niet volledig ophalen: ", schemaQuery.error instanceof Error ? schemaQuery.error.message : "onbekende fout"] }));
2158
+ return (_jsxs("p", { className: "text-sm text-destructive", children: ["De kenmerken uit Floriday konden niet worden geladen. Probeer het later opnieuw.", " ", _jsxs("details", { className: "text-xs opacity-80", children: [_jsx("summary", { className: "cursor-pointer", children: "Details voor beheer" }), schemaQuery.error instanceof Error ? schemaQuery.error.message : "onbekende fout"] })] }));
2159
2159
  }
2160
2160
  const hasRegulatoryLookup = /^\d{5,6}$/.test(String(data.vbnCode ?? "").trim());
2161
2161
  if (hasRegulatoryLookup && regulatoryQuery.isLoading) {
@@ -2280,7 +2280,7 @@ function StepKlanten({ data, update, floridayCustomers }) {
2280
2280
  });
2281
2281
  }, [floridayCustomers, search, onlySelected, selectedSet]);
2282
2282
  if (floridayCustomers.length === 0) {
2283
- return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen klantorganisaties geladen vanuit Floriday. Controleer de connectie of probeer later opnieuw." }));
2283
+ return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen klantorganisaties geladen vanuit Floriday. Controleer de Floriday-koppeling (Instellingen \u203A Floriday) of probeer later opnieuw." }));
2284
2284
  }
2285
2285
  const toggle = (id) => {
2286
2286
  if (selectedSet.has(id)) {
@@ -2326,7 +2326,7 @@ function StepFotos({ data, update, activeId }) {
2326
2326
  if (!files || files.length === 0)
2327
2327
  return;
2328
2328
  if (!activeId) {
2329
- toast.error("Geen actieve Floriday-connectie – kan foto's niet uploaden");
2329
+ toast.error("Geen actieve Floriday-koppeling – foto's kunnen niet worden geüpload");
2330
2330
  return;
2331
2331
  }
2332
2332
  const fileArr = Array.from(files);
@@ -2661,7 +2661,7 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
2661
2661
  packagingSupplierId: v,
2662
2662
  packagingSupplierName: sup?.name,
2663
2663
  }));
2664
- }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies leverancier..." }) }), _jsx(SelectContent, { children: PACKAGING_SUPPLIERS.map((s) => (_jsx(SelectItem, { value: s.id, children: s.name }, s.id))) })] }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Floricode VRS-materiaal UUID (optioneel)" }), _jsx(Input, { value: draft.packagingMaterialId ?? "", onChange: (e) => patch("packagingMaterialId", e.target.value.trim()), placeholder: "00000000-0000-0000-0000-000000000000", className: materialInvalid ? "border-destructive" : "" }), materialInvalid ? (_jsx("p", { className: "text-xs text-destructive", children: "Ongeldig UUID-formaat. Laat leeg of plak een echte VRS-materiaal UUID uit Floricode." })) : (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wanneer ingevuld gaat het als ", _jsx("code", { children: "customPackageId" }), "naar Floriday en vervangt het de VBN-fust."] }))] })] }), hasCustomPkg && (_jsx("div", { className: "rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs", children: "Custom package actief \u2014 VBN-fust hieronder wordt genegeerd in de Floriday-payload." }))] })), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: hasCustomPkg
2664
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies leverancier..." }) }), _jsx(SelectContent, { children: PACKAGING_SUPPLIERS.map((s) => (_jsx(SelectItem, { value: s.id, children: s.name }, s.id))) })] }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Floricode VRS-materiaal UUID (optioneel)" }), _jsx(Input, { value: draft.packagingMaterialId ?? "", onChange: (e) => patch("packagingMaterialId", e.target.value.trim()), placeholder: "00000000-0000-0000-0000-000000000000", className: materialInvalid ? "border-destructive" : "" }), materialInvalid ? (_jsx("p", { className: "text-xs text-destructive", children: "Deze code klopt niet. Laat het veld leeg of kopieer de materiaalcode uit Floricode." })) : (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wanneer ingevuld gaat het als ", _jsx("code", { children: "customPackageId" }), "naar Floriday en vervangt het de VBN-fust."] }))] })] }), hasCustomPkg && (_jsx("div", { className: "rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs", children: "Eigen verpakking gekozen; het VBN-fust hieronder wordt niet naar Floriday gestuurd." }))] })), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: hasCustomPkg
2665
2665
  ? "VBN-fust (genegeerd — custom package actief)"
2666
2666
  : draft.source === "SUPPLIER"
2667
2667
  ? "Fallback VBN-fust"
@@ -2839,7 +2839,7 @@ function StepAanbieden({ data, update, priceGroups, parentProductId }) {
2839
2839
  ? "Aanbod wordt aangemaakt direct na publiceren."
2840
2840
  : "Alleen artikel publiceren, geen aanbod aanmaken." })] }), _jsx(Switch, { checked: data.offerForSale, onCheckedChange: (v) => update("offerForSale", Boolean(v)) })] }), data.offerForSale && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Voorraad (stuks)" }), _jsx(Input, { type: "number", inputMode: "numeric", min: 0, step: 1, value: stockStr, onChange: (e) => update("saleStockPieces", Math.max(0, Math.floor(Number(e.target.value) || 0))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt als fysieke voorraad geboekt en via continuous-stock naar Floriday gepusht." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Basisprijs per stuk (EUR, excl. btw)" }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", value: priceStr, onChange: (e) => update("salePricePerPiece", Number(e.target.value) || 0) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Wordt gepusht als ", _jsx("code", { children: "weekly-base-supply" }), " \u2014 de standaard catalogusprijs voor klanten zonder eigen prijsgroep."] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aanbod geldig vanaf" }), _jsx(Input, { type: "date", value: data.offerStartDate, onChange: (e) => update("offerStartDate", e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Aanbod loopt automatisch tot 1 jaar na deze datum." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Zichtbaar voor" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: data.customerOrganizationIds.length
2841
2841
  ? `${data.customerOrganizationIds.length} geselecteerde klant(en)`
2842
- : "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }), _jsxs("div", { className: "space-y-2 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Prijzen per Floriday-prijsgroep" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Laat leeg om voor die groep g\u00E9\u00E9n afwijkende prijs te publiceren. Basisprijs blijft dan gelden." })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [priceGroups.length, " groep", priceGroups.length === 1 ? "" : "en"] })] }), priceGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-4", children: "Geen actieve prijsgroepen gevonden voor deze Floriday-connectie." })) : (_jsx("div", { className: "grid gap-3 sm:grid-cols-2 pt-2", children: priceGroups.map((g) => {
2842
+ : "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }), _jsxs("div", { className: "space-y-2 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Prijzen per Floriday-prijsgroep" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Laat leeg om voor die groep g\u00E9\u00E9n afwijkende prijs te publiceren. Basisprijs blijft dan gelden." })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [priceGroups.length, " groep", priceGroups.length === 1 ? "" : "en"] })] }), priceGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-4", children: "Geen actieve prijsgroepen gevonden voor deze Floriday-koppeling." })) : (_jsx("div", { className: "grid gap-3 sm:grid-cols-2 pt-2", children: priceGroups.map((g) => {
2843
2843
  const val = data.priceGroupPrices?.[g.id];
2844
2844
  return (_jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "text-sm", children: [g.name, _jsxs("span", { className: "ml-2 text-xs font-normal text-muted-foreground", children: [g.customerOrganizationIds.length, " klant", g.customerOrganizationIds.length === 1 ? "" : "en"] })] }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", placeholder: "\u20AC", value: val ? String(val) : "", onChange: (e) => setGroupPrice(g.id, e.target.value) })] }, g.id));
2845
2845
  }) }))] })] })), parentProductId && (_jsx(ProductSalePriceField, { productId: parentProductId, onSaved: (p) => update("salePricePerPiece", p) })), _jsx(VatSection, { data: data, update: update })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"FloridayInfoCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FloridayInfoCard.tsx"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,8EAA8E;IAC9E,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB,CAAC;CACH;AA6DD,wBAAgB,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,KAAK,sCAqRrE"}
1
+ {"version":3,"file":"FloridayInfoCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FloridayInfoCard.tsx"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,8EAA8E;IAC9E,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB,CAAC;CACH;AA6DD,wBAAgB,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,KAAK,sCAmRrE"}
@@ -75,7 +75,7 @@ export function FloridayInfoCard({ floridayTradeItemId, local }) {
75
75
  }, [floridayTradeItemId]);
76
76
  if (!floridayTradeItemId)
77
77
  return null;
78
- return (_jsx(Card, { className: "overflow-hidden", children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-semibold", children: "Floriday-informatie" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Rechtstreeks uit Floriday geladen \u2014 controle-informatie, wordt niet lokaal overschreven." })] }), info?.tradeItemVersion != null && (_jsxs(Badge, { variant: "secondary", className: "text-xs", children: ["v", info.tradeItemVersion] }))] }), loading && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Floriday-info laden\u2026" })), !loading && notFound && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900", children: ["Geen Floriday-record gevonden voor id", " ", _jsx("code", { className: "font-mono", children: floridayTradeItemId }), ". Wacht op de volgende sync of controleer de connectie."] })), info && (_jsxs(_Fragment, { children: [info.photos.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "rounded-md border overflow-hidden bg-muted/30", children: _jsx("img", { src: info.photos[activePhoto]?.url ?? info.photos[0].url, alt: info.name, className: "w-full max-h-80 object-contain" }) }), info.photos.length > 1 && (_jsx("div", { className: "flex gap-2 flex-wrap", children: info.photos.map((p, i) => (_jsx("button", { type: "button", onClick: () => setActivePhoto(i), className: `h-14 w-14 rounded border overflow-hidden ${i === activePhoto
78
+ return (_jsx(Card, { className: "overflow-hidden", children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-semibold", children: "Floriday-informatie" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Rechtstreeks uit Floriday geladen \u2014 controle-informatie, wordt niet lokaal overschreven." })] }), info?.tradeItemVersion != null && (_jsxs(Badge, { variant: "secondary", className: "text-xs", children: ["v", info.tradeItemVersion] }))] }), loading && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Floriday-info laden\u2026" })), !loading && notFound && (_jsx("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900", children: "Dit artikel is nog niet opgehaald uit Floriday. Klik op \u2018Bijwerken\u2019 en probeer opnieuw." })), info && (_jsxs(_Fragment, { children: [info.photos.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "rounded-md border overflow-hidden bg-muted/30", children: _jsx("img", { src: info.photos[activePhoto]?.url ?? info.photos[0].url, alt: info.name, className: "w-full max-h-80 object-contain" }) }), info.photos.length > 1 && (_jsx("div", { className: "flex gap-2 flex-wrap", children: info.photos.map((p, i) => (_jsx("button", { type: "button", onClick: () => setActivePhoto(i), className: `h-14 w-14 rounded border overflow-hidden ${i === activePhoto
79
79
  ? "ring-2 ring-primary"
80
80
  : "opacity-70 hover:opacity-100"}`, "aria-label": `Foto ${i + 1}`, children: _jsx("img", { src: p.url, alt: "", className: "h-full w-full object-cover" }) }, p.url))) }))] })), _jsxs("div", { className: "grid gap-x-8 gap-y-1 md:grid-cols-2", children: [_jsxs("div", { children: [_jsx(DiffField, { label: "Naam", floridayValue: info.name, localValue: local?.name ?? null }), _jsx(DiffField, { label: "Artikelnr", floridayValue: info.supplierArticleCode, localValue: local?.articleNumber ?? null }), _jsx(DiffField, { label: "GTIN", floridayValue: info.articleGtin, localValue: local?.gtin ?? null }), _jsx(Field, { label: "VBN-code", value: info.vbnProductCode }), _jsx(Field, { label: "Referentie", value: info.supplierReference })] }), _jsxs("div", { children: [_jsx(Field, { label: "Botanische naam", value: info.botanicalNames.join(", ") || null }), _jsx(Field, { label: "Land van herkomst", value: info.countryOfOrigin.join(", ") || null }), _jsx(Field, { label: "Seizoensperiode", value: info.seasonalPeriods.length > 0
81
81
  ? info.seasonalPeriods
@@ -35,12 +35,12 @@ export function resultText(r) {
35
35
  return `Week ${r.target_week ?? "?"} is gesloten in Floriday`;
36
36
  case "pending":
37
37
  case "processing":
38
- return r.last_error ? `Wacht op Floriday (laatste fout: ${r.last_error.slice(0, 80)})` : "Wacht op Floriday";
38
+ return "Wacht op Floriday";
39
39
  case "superseded":
40
40
  return "Vervangen door een nieuwere stand";
41
41
  case "dead_letter":
42
42
  case "failed":
43
- return r.last_error ? `Mislukt: ${r.last_error.slice(0, 120)}` : "Mislukt";
43
+ return "Niet aangekomen in Floriday. Klik op ‘Opnieuw proberen’.";
44
44
  default:
45
45
  return r.status;
46
46
  }
@@ -43,5 +43,5 @@ export function InventoryStatusBadge({ setting }) {
43
43
  ? `${dot.title}: ${ACTION_REASON_LABELS[setting.action_reason] ?? setting.action_reason}`
44
44
  : setting.last_sync_error
45
45
  ? `${dot.title}: ${setting.last_sync_error}`
46
- : dot.title, "aria-label": dot.title }), _jsx("span", { className: "tabular-nums text-xs text-muted-foreground", children: setting.desired_quantity ?? 0 }), setting.last_sync_error?.startsWith("Geen voorkeursmagazijn") && (_jsx("span", { className: "rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive", title: setting.last_sync_error, children: "Geen magazijn \u2014 kies er een op de Connectie-pagina" }))] }));
46
+ : dot.title, "aria-label": dot.title }), _jsx("span", { className: "tabular-nums text-xs text-muted-foreground", children: setting.desired_quantity ?? 0 }), setting.last_sync_error?.startsWith("Geen voorkeursmagazijn") && (_jsx("span", { className: "rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive", title: setting.last_sync_error, children: "Geen magazijn \u2014 kies er een in Instellingen \u203A Floriday" }))] }));
47
47
  }
@@ -137,10 +137,10 @@ export function ProductLinkCard({ tradeItemKey, productName }) {
137
137
  throw error;
138
138
  const rows = data ?? [];
139
139
  const n = rows[0]?.processed_items ?? 0;
140
- toast.success(`Backfill klaar: ${n} orderregel(s) verwerkt`);
140
+ toast.success(`Openstaande orders gereserveerd: ${n} orderregel(s)`);
141
141
  }
142
142
  catch (e) {
143
- toast.error(`Backfill mislukt: ${e.message}`);
143
+ toast.error("Openstaande orders reserveren is niet gelukt. Probeer het opnieuw.");
144
144
  }
145
145
  finally {
146
146
  setBusy(false);
@@ -151,9 +151,9 @@ export function ProductLinkCard({ tradeItemKey, productName }) {
151
151
  return "Sla het artikel eerst op om producten te kunnen koppelen.";
152
152
  }
153
153
  if (linked.length === 0) {
154
- return "Nog geen ERP-product gekoppeld. Zonder koppeling worden mailbox-orders niet automatisch van de voorraad afgeschreven.";
154
+ return "Nog geen eigen product gekoppeld. Zonder koppeling worden mailbox-orders niet automatisch van de voorraad afgeschreven.";
155
155
  }
156
- return "Bestellingen op deze ERP-producten reserveren automatisch op deze voorraadrij.";
156
+ return "Bestellingen op deze eigen producten reserveren automatisch op deze voorraadrij.";
157
157
  }, [tradeItemKey, linked.length]);
158
- return (_jsxs(Card, { children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-6 py-4", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full border border-primary/40 bg-primary/10 text-primary", children: _jsx(Link2, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium", children: "Gekoppelde ERP-producten" }), _jsx("div", { className: "text-xs text-muted-foreground", children: helpText })] }), _jsxs("div", { className: "flex gap-2", children: [productName && (_jsxs(Button, { variant: "outline", size: "sm", onClick: autoLinkByName, disabled: !tradeItemKey || busy, title: "Zoek ERP-producten met exact deze naam en koppel ze", children: [_jsx(Wand2, { className: "h-4 w-4 mr-1" }), "Auto op naam"] })), _jsxs(Button, { variant: "outline", size: "sm", onClick: backfillReservations, disabled: busy, title: "Reserveer openstaande orders retroactief", children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1" }), "Backfill"] })] })] }), _jsx(CardContent, { className: "pt-0 space-y-3", children: loading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Laden\u2026"] })) : (_jsxs(_Fragment, { children: [linked.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border", children: linked.map((p) => (_jsxs("li", { className: "flex items-center justify-between gap-2 px-3 py-2 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium", children: p.product ?? "(zonder naam)" }), p.barcode && (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Barcode: ", p.barcode] }))] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => unlinkProduct(p.id), disabled: busy, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))) })), _jsxs("div", { className: "space-y-2", children: [_jsx(Input, { placeholder: "Zoek ERP-product op naam of SKU\u2026", value: query, onChange: (e) => setQuery(e.target.value), disabled: !tradeItemKey || busy }), candidates.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border max-h-56 overflow-auto", children: candidates.map((p) => (_jsxs("li", { className: "flex items-center justify-between gap-2 px-3 py-2 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate", children: p.product ?? "(zonder naam)" }), p.barcode && (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Barcode: ", p.barcode] }))] }), _jsx(Button, { size: "sm", onClick: () => linkProduct(p.id), disabled: busy, children: "Koppel" })] }, p.id))) }))] })] })) })] }));
158
+ return (_jsxs(Card, { children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-6 py-4", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full border border-primary/40 bg-primary/10 text-primary", children: _jsx(Link2, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium", children: "Gekoppelde eigen producten" }), _jsx("div", { className: "text-xs text-muted-foreground", children: helpText })] }), _jsxs("div", { className: "flex gap-2", children: [productName && (_jsxs(Button, { variant: "outline", size: "sm", onClick: autoLinkByName, disabled: !tradeItemKey || busy, title: "Zoek eigen producten met exact deze naam en koppel ze", children: [_jsx(Wand2, { className: "h-4 w-4 mr-1" }), "Auto op naam"] })), _jsxs(Button, { variant: "outline", size: "sm", onClick: backfillReservations, disabled: busy, title: "Openstaande orders reserveren", children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1" }), "Openstaande orders reserveren"] })] })] }), _jsx(CardContent, { className: "pt-0 space-y-3", children: loading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Laden\u2026"] })) : (_jsxs(_Fragment, { children: [linked.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border", children: linked.map((p) => (_jsxs("li", { className: "flex items-center justify-between gap-2 px-3 py-2 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium", children: p.product ?? "(zonder naam)" }), p.barcode && (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Barcode: ", p.barcode] }))] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => unlinkProduct(p.id), disabled: busy, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))) })), _jsxs("div", { className: "space-y-2", children: [_jsx(Input, { placeholder: "Zoek eigen product op naam of artikelcode\u2026", value: query, onChange: (e) => setQuery(e.target.value), disabled: !tradeItemKey || busy }), candidates.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border max-h-56 overflow-auto", children: candidates.map((p) => (_jsxs("li", { className: "flex items-center justify-between gap-2 px-3 py-2 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate", children: p.product ?? "(zonder naam)" }), p.barcode && (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Barcode: ", p.barcode] }))] }), _jsx(Button, { size: "sm", onClick: () => linkProduct(p.id), disabled: busy, children: "Koppel" })] }, p.id))) }))] })] })) })] }));
159
159
  }
@@ -1 +1 @@
1
- {"version":3,"file":"StepSamenstelling.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StepSamenstelling.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAsB5E,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,yBAAyB,EAAE,CAAC;IACtC,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C;AAUD,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,UAAU,EACV,QAAQ,EACR,kBAAkB,GACnB,EAAE,KAAK,+BA+ZP"}
1
+ {"version":3,"file":"StepSamenstelling.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StepSamenstelling.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAsB5E,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,yBAAyB,EAAE,CAAC;IACtC,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C;AA6FD,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,UAAU,EACV,QAAQ,EACR,kBAAkB,GACnB,EAAE,KAAK,+BA+aP"}
@@ -3,6 +3,88 @@ import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { Button, Input, Popover, PopoverContent, PopoverTrigger, toast, } from "@flowselections/core";
4
4
  import { Loader2, Package, Plus, Search, Trash2 } from "lucide-react";
5
5
  import { supabase } from "../../integrations/supabase/client";
6
+ /** Vanaf dit aantal per stuk vragen we of de verhouding klopt. */
7
+ const RATIO_WARN_THRESHOLD = 10;
8
+ const MAX_DEPTH = 6;
9
+ function fmt(n) {
10
+ return n.toLocaleString("nl-NL", { maximumFractionDigits: 2 });
11
+ }
12
+ /**
13
+ * Telt per eindonderdeel hoeveel er in 1 stuk van het artikel zit, door alle
14
+ * lagen van de samenstelling heen (op id). Een groep telt mee als haar eerste lid.
15
+ */
16
+ async function unlikelyRatios(parentId, baseRows) {
17
+ const totals = new Map();
18
+ const bomCache = new Map();
19
+ const groupCache = new Map();
20
+ const childrenOf = async (pid) => {
21
+ if (bomCache.has(pid))
22
+ return bomCache.get(pid);
23
+ const { data } = await supabase
24
+ .from("product_bom_items")
25
+ .select("component_product_id,quantity")
26
+ .eq("parent_product_id", pid)
27
+ .is("packing_configuration_id", null);
28
+ const list = (data ?? [])
29
+ .filter((r) => r.component_product_id)
30
+ .map((r) => ({ id: r.component_product_id, qty: Number(r.quantity ?? 1) }));
31
+ bomCache.set(pid, list);
32
+ return list;
33
+ };
34
+ const groupMemberOf = async (pid) => {
35
+ if (groupCache.has(pid))
36
+ return groupCache.get(pid);
37
+ const { data: g } = await supabase
38
+ .from("product_groups")
39
+ .select("id")
40
+ .eq("head_product_id", pid)
41
+ .eq("is_active", true)
42
+ .order("updated_at", { ascending: false })
43
+ .limit(1)
44
+ .maybeSingle();
45
+ let member = null;
46
+ if (g?.id) {
47
+ const { data: m } = await supabase
48
+ .from("product_group_items")
49
+ .select("trade_item_id")
50
+ .eq("product_group_id", g.id)
51
+ .order("sort_order", { ascending: true })
52
+ .limit(1)
53
+ .maybeSingle();
54
+ member = m?.trade_item_id ?? null;
55
+ }
56
+ groupCache.set(pid, member);
57
+ return member;
58
+ };
59
+ const walk = async (pid, mult, depth) => {
60
+ if (depth > MAX_DEPTH)
61
+ return;
62
+ const kids = await childrenOf(pid);
63
+ if (kids.length > 0) {
64
+ for (const k of kids)
65
+ await walk(k.id, mult * k.qty, depth + 1);
66
+ return;
67
+ }
68
+ const member = await groupMemberOf(pid);
69
+ if (member && member !== pid) {
70
+ await walk(member, mult, depth + 1);
71
+ return;
72
+ }
73
+ totals.set(pid, (totals.get(pid) ?? 0) + mult);
74
+ };
75
+ for (const r of baseRows) {
76
+ if (r.component_product_id)
77
+ await walk(r.component_product_id, Number(r.quantity) || 0, 1);
78
+ }
79
+ const over = Array.from(totals.entries()).filter(([, q]) => q >= RATIO_WARN_THRESHOLD);
80
+ if (over.length === 0)
81
+ return [];
82
+ const ids = [...over.map(([id]) => id), ...(parentId ? [parentId] : [])];
83
+ const { data: prods } = await supabase.from("products").select("id,product").in("id", ids);
84
+ const nameOf = new Map((prods ?? []).map((p) => [p.id, String(p.product ?? "")]));
85
+ const parent = (parentId && nameOf.get(parentId)) || "stuk van dit artikel";
86
+ return over.map(([id, qty]) => ({ parent, name: nameOf.get(id) || "onderdeel", qty }));
87
+ }
6
88
  function newRowId() {
7
89
  return `new:${typeof crypto !== "undefined" && "randomUUID" in crypto
8
90
  ? crypto.randomUUID()
@@ -166,6 +248,21 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
166
248
  if (dirty.length === 0)
167
249
  return;
168
250
  setSaving(true);
251
+ // Verhoudingscontrole: alle lagen meetellen, nooit blokkeren.
252
+ try {
253
+ const warnings = await unlikelyRatios(productId, rows.filter((r) => r.packing_configuration_id === null));
254
+ if (warnings.length > 0) {
255
+ const ok = window.confirm(warnings.map((w) => `Let op: 1 ${w.parent} bevat ${fmt(w.qty)} ${w.name}. Klopt dat?`).join("\n") +
256
+ "\n\nKies OK om toch op te slaan, of Annuleren om aan te passen.");
257
+ if (!ok) {
258
+ setSaving(false);
259
+ return;
260
+ }
261
+ }
262
+ }
263
+ catch {
264
+ // Controle mislukt: opslaan gaat gewoon door.
265
+ }
169
266
  try {
170
267
  const inserts = dirty
171
268
  .filter((r) => r._new)
@@ -193,7 +193,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
193
193
  /** Stap 1: bestand lezen en per rij controleren (nog niets opgeslagen). */
194
194
  const handleFile = async (file) => {
195
195
  if (!activeConnectionId) {
196
- toast.error("Geen actieve Floriday-connectie");
196
+ toast.error("Geen actieve Floriday-koppeling. Kijk bij Instellingen › Floriday.");
197
197
  return;
198
198
  }
199
199
  setBusy("lezen");
@@ -1 +1 @@
1
- {"version":3,"file":"StockPushMonitor.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockPushMonitor.tsx"],"names":[],"mappings":"AA4NA,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BAiNjF"}
1
+ {"version":3,"file":"StockPushMonitor.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockPushMonitor.tsx"],"names":[],"mappings":"AA4NA,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BAkNjF"}
@@ -60,7 +60,7 @@ function explainError(raw) {
60
60
  const m = raw.toLowerCase();
61
61
  if (m.includes("token") || m.includes("requested path is invalid")) {
62
62
  return {
63
- text: "De Floriday-Authenticatiemodule kon geen geldig token ophalen.",
63
+ text: "De koppeling met Floriday werkt niet; de voorraad wordt niet doorgegeven. Neem contact op met Flow Selections.",
64
64
  action: "Controleer in de Floriday-Authenticatiemodule het adres van Central Auth en of de gepubliceerde versie actueel is. Plan de opdracht daarna opnieuw in.",
65
65
  };
66
66
  }
@@ -89,7 +89,7 @@ function displayStatus(job) {
89
89
  return {
90
90
  label: "Vastgelopen",
91
91
  tone: "error",
92
- hint: "De verwerker heeft deze opdracht opgepakt maar nooit afgerond. Hij wordt automatisch opnieuw opgepakt.",
92
+ hint: "Deze wijziging bleef hangen en wordt automatisch opnieuw verstuurd.",
93
93
  };
94
94
  }
95
95
  return { label: "Bezig", tone: "warn" };
@@ -98,7 +98,7 @@ function displayStatus(job) {
98
98
  return {
99
99
  label: "Geen magazijn, niet verzonden",
100
100
  tone: "error",
101
- hint: job.skipReason ?? "Geen voorkeursmagazijn voor deze koppeling; kies er een op de Connectie-pagina",
101
+ hint: job.skipReason ?? "Geen voorkeursmagazijn voor deze koppeling; kies er een in Instellingen › Floriday",
102
102
  };
103
103
  }
104
104
  if (job.status === "skipped_no_price") {
@@ -237,9 +237,9 @@ export function StockPushMonitor({ connectionId }) {
237
237
  return `laatste: ${displayStatus(last).label} om ${timeLabel(last.createdAt)}`;
238
238
  }, [rows, jobs.isError, jobsError]);
239
239
  const stuckCount = rows.filter((r) => displayStatus(r).label === "Vastgelopen").length;
240
- return (_jsxs("section", { className: "rounded-lg border bg-card", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium", onClick: () => setOpen((v) => !v), children: [open ? (_jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })), "Voorraadsync naar Floriday testen", _jsx("span", { className: "ml-auto text-xs font-normal text-muted-foreground", children: summary })] }), open && (_jsxs("div", { className: "space-y-4 border-t px-4 py-4 text-sm", children: [_jsx("p", { className: "text-muted-foreground", children: "Elke voorraadwijziging van een gekoppeld artikel zet automatisch opdrachten in de wachtrij. De Floriday-Authenticatiemodule verwerkt die elke minuut. Hieronder plan je een testopdracht in met de huidige voorraadstand, zonder iets te wijzigen." }), _jsxs("p", { className: "rounded-md border bg-muted/40 px-3 py-2 text-muted-foreground", children: ["E\u00E9n wijziging levert bewust meerdere berichten op: \u00E9\u00E9n met de", " ", _jsx("span", { className: "font-medium text-foreground", children: "actuele voorraad" }), " en daarnaast \u00E9\u00E9n per week met de ", _jsx("span", { className: "font-medium text-foreground", children: "verwachte beschikbaarheid" }), " voor de komende weken. Ze lijken op elkaar, maar gaan elk over een andere periode \u2014 zie de kolom \u201CGeldt voor\u201D. Plan je opnieuw een wijziging in, dan wordt een nog wachtende opdracht voor dezelfde week vervangen."] }), blockade.data?.blocked && (_jsxs("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive", children: [_jsx("span", { className: "font-medium", children: "Actie nodig: " }), "de Floriday-Authenticatiemodule krijgt geen geldig token, dus elke push mislukt meteen. Dit lost zichzelf niet op \u2014 controleer daar het adres van Central Auth en de actieve inloggegevens voor deze omgeving.", blockade.data.message && (_jsxs("span", { className: "block opacity-80", children: ["Melding: ", blockade.data.message] }))] })), stuckCount > 0 && (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-800", children: [stuckCount === 1 ? "Eén opdracht is" : `${stuckCount} opdrachten zijn`, " door de verwerker opgepakt maar nooit afgerond. Ze worden na vijf minuten automatisch opnieuw opgepakt."] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Select, { value: selected, onValueChange: setSelected, disabled: !connectionId || items.isLoading || itemList.length === 0, children: [_jsx(SelectTrigger, { className: "h-9 w-[22rem]", children: _jsx(SelectValue, { placeholder: items.isLoading ? "Artikelen laden…" : "Geen gekoppelde artikelen gevonden" }) }), _jsx(SelectContent, { className: "max-h-72", children: itemList.map((i) => (_jsx(SelectItem, { value: i.id, children: _jsxs("span", { className: "flex w-full items-center justify-between gap-3", children: [_jsx("span", { className: "truncate", children: i.name }), _jsxs("span", { className: "tabular-nums text-muted-foreground", children: [i.stock, " stuks"] })] }) }, i.id))) })] }), _jsxs(Button, { type: "button", disabled: !selected || mutation.isPending, onClick: () => selected && mutation.mutate(selected), children: [_jsx(Send, { className: "mr-2 h-4 w-4" }), "Testpush uitvoeren"] }), _jsxs(Button, { type: "button", variant: "outline", disabled: jobs.isFetching, onClick: () => void jobs.refetch(), children: [_jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${jobs.isFetching ? "animate-spin" : ""}` }), "Ververs"] })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left text-xs", children: [_jsx("thead", { className: "text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-1 pr-3 font-medium", children: "Artikel" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Geldt voor" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Stuks" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Status" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Pogingen" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Aangemaakt" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Bijgewerkt" }), _jsx("th", { className: "py-1 font-medium", children: "Melding" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 8, className: "py-3 text-muted-foreground", children: jobs.isError
240
+ return (_jsxs("section", { className: "rounded-lg border bg-card", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium", onClick: () => setOpen((v) => !v), children: [open ? (_jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })), "Voorraad naar Floriday testen", _jsx("span", { className: "ml-auto text-xs font-normal text-muted-foreground", children: summary })] }), open && (_jsxs("div", { className: "space-y-4 border-t px-4 py-4 text-sm", children: [_jsx("p", { className: "text-muted-foreground", children: "Elke voorraadwijziging van een gekoppeld artikel zet automatisch opdrachten in de wachtrij. De Floriday-Authenticatiemodule verwerkt die elke minuut. Hieronder plan je een testopdracht in met de huidige voorraadstand, zonder iets te wijzigen." }), _jsxs("p", { className: "rounded-md border bg-muted/40 px-3 py-2 text-muted-foreground", children: ["E\u00E9n wijziging levert bewust meerdere berichten op: \u00E9\u00E9n met de", " ", _jsx("span", { className: "font-medium text-foreground", children: "actuele voorraad" }), " en daarnaast \u00E9\u00E9n per week met de ", _jsx("span", { className: "font-medium text-foreground", children: "verwachte beschikbaarheid" }), " voor de komende weken. Ze lijken op elkaar, maar gaan elk over een andere periode \u2014 zie de kolom \u201CGeldt voor\u201D. Plan je opnieuw een wijziging in, dan wordt een nog wachtende opdracht voor dezelfde week vervangen."] }), blockade.data?.blocked && (_jsxs("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive", children: [_jsx("span", { className: "font-medium", children: "Actie nodig: " }), "De koppeling met Floriday werkt niet; de voorraad wordt niet doorgegeven. Neem contact op met Flow Selections.", blockade.data.message && (_jsxs("details", { className: "mt-1 opacity-80", children: [_jsx("summary", { className: "cursor-pointer", children: "Details voor beheer" }), blockade.data.message] }))] })), stuckCount > 0 && (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-800", children: [stuckCount === 1 ? "Eén wijziging bleef" : `${stuckCount} wijzigingen bleven`, " hangen en ", stuckCount === 1 ? "wordt" : "worden", " automatisch opnieuw verstuurd."] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Select, { value: selected, onValueChange: setSelected, disabled: !connectionId || items.isLoading || itemList.length === 0, children: [_jsx(SelectTrigger, { className: "h-9 w-[22rem]", children: _jsx(SelectValue, { placeholder: items.isLoading ? "Artikelen laden…" : "Geen gekoppelde artikelen gevonden" }) }), _jsx(SelectContent, { className: "max-h-72", children: itemList.map((i) => (_jsx(SelectItem, { value: i.id, children: _jsxs("span", { className: "flex w-full items-center justify-between gap-3", children: [_jsx("span", { className: "truncate", children: i.name }), _jsxs("span", { className: "tabular-nums text-muted-foreground", children: [i.stock, " stuks"] })] }) }, i.id))) })] }), _jsxs(Button, { type: "button", disabled: !selected || mutation.isPending, onClick: () => selected && mutation.mutate(selected), children: [_jsx(Send, { className: "mr-2 h-4 w-4" }), "Test versturen"] }), _jsxs(Button, { type: "button", variant: "outline", disabled: jobs.isFetching, onClick: () => void jobs.refetch(), children: [_jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${jobs.isFetching ? "animate-spin" : ""}` }), "Ververs"] })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left text-xs", children: [_jsx("thead", { className: "text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-1 pr-3 font-medium", children: "Artikel" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Geldt voor" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Stuks" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Status" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Pogingen" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Aangemaakt" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Bijgewerkt" }), _jsx("th", { className: "py-1 font-medium", children: "Melding" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 8, className: "py-3 text-muted-foreground", children: jobs.isError
241
241
  ? `Pushes konden niet worden geladen${jobsError?.code ? ` (fout ${jobsError.code})` : ""}: ${jobsError?.message ?? "onbekende fout"}`
242
- : "Er staan nog geen push-opdrachten in de wachtrij." }) })), rows.map((r) => {
242
+ : "Er is nog geen voorraad naar Floriday gestuurd." }) })), rows.map((r) => {
243
243
  const status = displayStatus(r);
244
244
  const detail = explainError(r.lastError);
245
245
  const scope = scopeLabel(r);
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAyGA,wBAAgB,YAAY,gCA+V3B"}
1
+ {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAyGA,wBAAgB,YAAY,gCAmX3B"}
@@ -56,6 +56,7 @@ export function VoorraadPage() {
56
56
  const [drawerOpen, setDrawerOpen] = useState(false);
57
57
  // Artikelen zonder geldige prijs per stuk (eenheid "stuks", prijs > 0), op id.
58
58
  const [noPriceIds, setNoPriceIds] = useState(new Set());
59
+ const [noPriceBreakdown, setNoPriceBreakdown] = useState({ empty: 0, zero: 0, unit: 0 });
59
60
  const [noPriceOnly, setNoPriceOnly] = useState(false);
60
61
  useEffect(() => {
61
62
  let cancelled = false;
@@ -66,12 +67,24 @@ export function VoorraadPage() {
66
67
  if (cancelled)
67
68
  return;
68
69
  const s = new Set();
70
+ const b = { empty: 0, zero: 0, unit: 0 };
69
71
  for (const p of (data ?? [])) {
70
72
  const isPiece = (p.unit ?? "").trim().toLowerCase() === "stuks";
71
- if (!isPiece || !(Number(p.sale_price) > 0))
73
+ if (!isPiece) {
72
74
  s.add(p.id);
75
+ b.unit++;
76
+ }
77
+ else if (p.sale_price == null) {
78
+ s.add(p.id);
79
+ b.empty++;
80
+ }
81
+ else if (!(Number(p.sale_price) > 0)) {
82
+ s.add(p.id);
83
+ b.zero++;
84
+ }
73
85
  }
74
86
  setNoPriceIds(s);
87
+ setNoPriceBreakdown(b);
75
88
  });
76
89
  return () => {
77
90
  cancelled = true;
@@ -146,7 +159,13 @@ export function VoorraadPage() {
146
159
  { id: "out_of_stock", label: "Niet op voorraad", count: counts.outOfStock },
147
160
  { id: "below_min", label: "Onder minimum", count: counts.belowMin },
148
161
  ];
149
- return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-[12rem] flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, disabled: !activeId, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _jsx(StockIntegrityAlert, {}), _jsx(SupplyReadScopeAlert, {}), _jsx(StockSyncStatus, {}), noPriceIds.size > 0 && (_jsxs("div", { className: "flex flex-wrap items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-800", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("span", { className: "font-medium", children: [noPriceIds.size, " artikel", noPriceIds.size === 1 ? "" : "en", " staan niet in Floriday omdat de prijs ontbreekt."] }), " ", "Kies \"Toon deze artikelen\", open een artikel en vul bij Artikel regulier de verkoopprijs per stuk (excl. btw) in. De voorraadsturing van elk artikel heeft ook een link \"Prijs invullen\"."] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setNoPriceOnly((v) => !v), children: noPriceOnly ? "Alle artikelen tonen" : "Toon deze artikelen" })] })), auth.isAdmin && _jsx(StockPushMonitor, { connectionId: activeId ?? null }), connError && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Floriday-connectie laden mislukt: ", connError] })), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock") }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
162
+ return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-[12rem] flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, disabled: !activeId, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _jsx(StockIntegrityAlert, {}), _jsx(SupplyReadScopeAlert, {}), _jsx(StockSyncStatus, {}), noPriceIds.size > 0 && (_jsxs("div", { className: "flex flex-wrap items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-800", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("span", { className: "font-medium", children: [noPriceIds.size, " artikel", noPriceIds.size === 1 ? " staat" : "en staan", " niet in Floriday zonder geldige prijs:", " ", [
163
+ noPriceBreakdown.empty > 0 ? `${noPriceBreakdown.empty} zonder verkoopprijs` : null,
164
+ noPriceBreakdown.zero > 0 ? `${noPriceBreakdown.zero} met prijs € 0,00` : null,
165
+ noPriceBreakdown.unit > 0 ? `${noPriceBreakdown.unit} met een andere eenheid dan stuks` : null,
166
+ ]
167
+ .filter(Boolean)
168
+ .join(", "), "."] }), " ", "Kies \"Toon deze artikelen\", open een artikel en vul bij Artikel regulier de verkoopprijs per stuk (excl. btw) in. De voorraadsturing van elk artikel heeft ook een link \"Prijs invullen\"."] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setNoPriceOnly((v) => !v), children: noPriceOnly ? "Alle artikelen tonen" : "Toon deze artikelen" })] })), auth.isAdmin && _jsx(StockPushMonitor, { connectionId: activeId ?? null }), connError && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Floriday-koppeling laden mislukt. ", _jsxs("details", { className: "inline", children: [_jsx("summary", { className: "cursor-pointer inline", children: "Details voor beheer" }), connError] })] })), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock") }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
150
169
  const active = stockFilter === t.id;
151
170
  return (_jsxs("button", { type: "button", onClick: () => setStockFilter(t.id), className: `relative py-3 px-3 text-sm font-medium whitespace-nowrap transition-colors ${active
152
171
  ? "text-foreground"
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadTable.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAkBzE,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,EACP,aAAa,EACb,QAAQ,EACR,cAAc,EACd,WAAW,EACX,cAAc,GACf,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,+BAoPA"}
1
+ {"version":3,"file":"VoorraadTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadTable.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AA6EzE,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,EACP,aAAa,EACb,QAAQ,EACR,cAAc,EACd,WAAW,EACX,cAAc,GACf,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,+BAuSA"}