@flowselections/floriday-voorraad 1.0.14 → 1.0.16

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 (45) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.js +48 -7
  3. package/dist-lib/components/voorraad/ExcelExportButton.d.ts +2 -1
  4. package/dist-lib/components/voorraad/ExcelExportButton.d.ts.map +1 -1
  5. package/dist-lib/components/voorraad/ExcelExportButton.js +24 -5
  6. package/dist-lib/components/voorraad/ProductLinkCard.d.ts +13 -0
  7. package/dist-lib/components/voorraad/ProductLinkCard.d.ts.map +1 -0
  8. package/dist-lib/components/voorraad/ProductLinkCard.js +159 -0
  9. package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -1
  10. package/dist-lib/components/voorraad/StockBulkButtons.js +88 -7
  11. package/dist-lib/hooks/useTradeItemSpec.d.ts.map +1 -1
  12. package/dist-lib/hooks/useTradeItemSpec.js +4 -1
  13. package/dist-lib/hooks/useVoorraadData.d.ts +5 -4
  14. package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
  15. package/dist-lib/hooks/useVoorraadData.js +40 -6
  16. package/dist-lib/index.d.ts +1 -0
  17. package/dist-lib/index.d.ts.map +1 -1
  18. package/dist-lib/index.js +1 -0
  19. package/dist-lib/integrations/supabase/auth-middleware.d.ts +743 -0
  20. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/client.d.ts +743 -0
  22. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  23. package/dist-lib/integrations/supabase/client.server.d.ts +743 -0
  24. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/types.d.ts +763 -0
  26. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  27. package/dist-lib/lib/ensure-article-codes.d.ts +8 -0
  28. package/dist-lib/lib/ensure-article-codes.d.ts.map +1 -0
  29. package/dist-lib/lib/ensure-article-codes.js +78 -0
  30. package/dist-lib/lib/floricode-required-features.functions.d.ts +743 -0
  31. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  32. package/dist-lib/lib/floriday-client.d.ts +6 -0
  33. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  34. package/dist-lib/lib/floriday-gateway.functions.d.ts +2972 -0
  35. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/floriday-payload.d.ts +19 -5
  37. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  38. package/dist-lib/lib/floriday-payload.js +106 -33
  39. package/dist-lib/lib/floriday-warehouse.functions.d.ts +32228 -0
  40. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -0
  41. package/dist-lib/lib/floriday-warehouse.functions.js +98 -0
  42. package/dist-lib/lib/floriday-writes.functions.d.ts +1616 -130
  43. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/floriday-writes.functions.js +34 -6
  45. package/package.json +7 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AAyGA,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,GACZ,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,2CAoqBjD"}
1
+ {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA6GA,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,GACZ,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,2CA2qBjD"}
@@ -8,13 +8,15 @@ import { useFlorydayCustomers } from "../../hooks/useFlorydayCustomers";
8
8
  import { floriday } from "../../lib/floriday-client";
9
9
  import { listFloricode } from "../../lib/floricode-client";
10
10
  import { supabase } from "../../integrations/supabase/client";
11
- import { buildAddTradeItemPayload, emptyWizardData, floridayCacheToWizardData, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
11
+ import { buildAddTradeItemPayload, buildUpdateTradeItemPayload, emptyWizardData, floridayCacheToWizardData, PACKAGING_SUPPLIERS, validateStep, } from "../../lib/floriday-payload";
12
12
  import { deleteLocalTradeItem, getLocalTradeItem, upsertLocalTradeItem, } from "../../hooks/useLocalTradeItems";
13
13
  import { useTradeItemSpec } from "../../hooks/useTradeItemSpec";
14
14
  import { useVbnRegulatoryFeatures } from "../../hooks/useVbnRegulatoryFeatures";
15
15
  import { useCodeList, useTradeItemFormSchema } from "../../hooks/useFloriday";
16
- import floridayLogo from "../../assets/floriday-logo.png.asset.json";
16
+ const floridayLogo = "/flowselections-assets/Voorraad/floriday-logo.png";
17
17
  import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
18
+ import { ProductLinkCard } from "../../components/voorraad/ProductLinkCard";
19
+ import { stockKeyFor } from "../../hooks/useTradeItemStock";
18
20
  const empty = emptyWizardData;
19
21
  const STEPS = [
20
22
  { key: "vbn", label: "VBN-code" },
@@ -23,6 +25,7 @@ const STEPS = [
23
25
  { key: "eigenschappen", label: "Eigenschappen" },
24
26
  { key: "klanten", label: "Klanten" },
25
27
  { key: "fotos", label: "Foto's" },
28
+ { key: "warehouse", label: "Warehouse" },
26
29
  { key: "beladingen", label: "Beladingen" },
27
30
  { key: "verpakking", label: "Aanvullende verpakkingsmaterialen" },
28
31
  { key: "samenstelling", label: "Samenstelling", disabled: true },
@@ -300,7 +303,8 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
300
303
  try {
301
304
  if (isEditingPublished && tradeItemId) {
302
305
  // PUT vervangt het bestaande trade item in Floriday
303
- const payload = { ...buildAddTradeItemPayload(data), tradeItemId };
306
+ // PUT /trade-items/{id} — body volgens TradeItemUpdate (zonder tradeItemId + supplierArticleCode)
307
+ const payload = buildUpdateTradeItemPayload(data);
304
308
  const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
305
309
  if (!res?.attemptId) {
306
310
  throw new Error("Gateway gaf geen attemptId terug — wijziging niet bevestigd");
@@ -309,7 +313,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
309
313
  try {
310
314
  await supabase
311
315
  .from("floriday_trade_items_cache")
312
- .update({ data: payload, updated_at: new Date().toISOString() })
316
+ .update({ data: { ...payload, tradeItemId }, updated_at: new Date().toISOString() })
313
317
  .eq("connection_id", activeId)
314
318
  .eq("floriday_id", tradeItemId);
315
319
  }
@@ -412,7 +416,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
412
416
  if (!activeConnection) {
413
417
  return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-connectie gevonden. Voeg er eerst een toe in Instellingen." }) }));
414
418
  }
415
- return (_jsxs("main", { className: "mx-auto w-full max-w-3xl space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/voorraad", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Terug naar voorraad"] }) }), !isEditingPublished && (_jsxs(Button, { variant: "outline", size: "sm", className: "ml-auto", onClick: saveDraft, disabled: submitting || !vbnValid, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), isEditingDraft ? "Concept bijwerken" : "Opslaan als concept"] })), isEditingDraft && (_jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Lokaal artikel verwijderen?" }), _jsx(AlertDialogDescription, { children: "Dit artikel is nog niet gepubliceerd naar Floriday. Verwijderen is definitief." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteDraft, children: "Verwijderen" })] })] })] }))] }), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo.url, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
419
+ return (_jsxs("main", { className: "mx-auto w-full max-w-3xl space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: "/voorraad", children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Terug naar voorraad"] }) }), !isEditingPublished && (_jsxs(Button, { variant: "outline", size: "sm", className: "ml-auto", onClick: saveDraft, disabled: submitting || !vbnValid, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(ClipboardList, { className: "mr-2 h-4 w-4" })), isEditingDraft ? "Concept bijwerken" : "Opslaan als concept"] })), isEditingDraft && (_jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Lokaal artikel verwijderen?" }), _jsx(AlertDialogDescription, { children: "Dit artikel is nog niet gepubliceerd naar Floriday. Verwijderen is definitief." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteDraft, children: "Verwijderen" })] })] })] }))] }), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wijzigingen worden direct naar Floriday verstuurd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsxs(_Fragment, { children: [_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: stockKeyFor({ tradeItemId: tradeItemId ?? null, draftId: draftId ?? null }), productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
416
420
  const isOpen = i === step;
417
421
  const isValid = stepValidity[i];
418
422
  const isVisited = i < step || isEditing;
@@ -431,7 +435,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, } = {}) {
431
435
  ? "border-primary bg-primary text-primary-foreground"
432
436
  : status === "invalid"
433
437
  ? "border-destructive bg-destructive text-destructive-foreground"
434
- : "border-muted-foreground/30 text-muted-foreground"}`, children: status === "valid" ? (_jsx(Check, { className: "h-4 w-4" })) : status === "invalid" ? ("!") : (i + 1) }), _jsx("span", { className: `min-w-0 truncate font-medium ${status === "untouched" ? "text-muted-foreground" : ""}`, children: s.label }), !isOpen && summary && (_jsx("span", { className: `max-w-xs truncate text-xs ${status === "invalid" ? "text-destructive" : "text-muted-foreground"}`, children: status === "invalid" ? `Onvolledig — ${summary}` : summary }))] }), isOpen && (_jsxs(CardContent, { className: "space-y-4 pt-0", children: [_jsx("div", { "data-wizard-form-content": true, className: "w-full min-w-0", children: _jsx(StepBody, { stepKey: s.key, data: data, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo.url, alt: "Floriday", className: "h-7 w-7 shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Label, { htmlFor: "publish-to-floriday", className: "font-medium cursor-pointer", children: "Publiceren naar Floriday" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: publishToFloriday
438
+ : "border-muted-foreground/30 text-muted-foreground"}`, children: status === "valid" ? (_jsx(Check, { className: "h-4 w-4" })) : status === "invalid" ? ("!") : (i + 1) }), _jsx("span", { className: `min-w-0 truncate font-medium ${status === "untouched" ? "text-muted-foreground" : ""}`, children: s.label }), !isOpen && summary && (_jsx("span", { className: `max-w-xs truncate text-xs ${status === "invalid" ? "text-destructive" : "text-muted-foreground"}`, children: status === "invalid" ? `Onvolledig — ${summary}` : summary }))] }), isOpen && (_jsxs(CardContent, { className: "space-y-4 pt-0", children: [_jsx("div", { "data-wizard-form-content": true, className: "w-full min-w-0", children: _jsx(StepBody, { stepKey: s.key, data: data, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes }) }), isLastStep && !isEditingPublished && (_jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 flex items-start gap-3", children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-7 w-7 shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Label, { htmlFor: "publish-to-floriday", className: "font-medium cursor-pointer", children: "Publiceren naar Floriday" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: publishToFloriday
435
439
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
436
440
  : "Het artikel wordt alleen lokaal opgeslagen. Je kunt het later vanuit de voorraadlijst alsnog publiceren naar Floriday." })] }), _jsx(Switch, { id: "publish-to-floriday", checked: publishToFloriday, onCheckedChange: setPublishToFloriday })] })), (isLastStep || isEditing) && invalidStepCount > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive flex items-center justify-between gap-3", children: [_jsxs("span", { children: ["Nog ", invalidStepCount, " stap", invalidStepCount === 1 ? "" : "pen", " onvolledig"] }), firstInvalidIndex >= 0 && (_jsxs("button", { type: "button", onClick: () => setStep(firstInvalidIndex), className: "underline font-medium", children: ["Ga naar ", steps[firstInvalidIndex].label] }))] })), _jsx("div", { className: "flex justify-end pt-2", children: isLastStep || isEditing ? (_jsxs(Button, { onClick: submit, disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), isEditingPublished
437
441
  ? "Wijzigingen opslaan"
@@ -475,6 +479,8 @@ function summaryFor(key, d) {
475
479
  : "Alle klanten";
476
480
  case "fotos":
477
481
  return `${d.images.length} foto('s)`;
482
+ case "warehouse":
483
+ return d.warehouseId ? "Gekozen" : "Niet gekozen";
478
484
  case "beladingen":
479
485
  return d.packingConfigurations.length
480
486
  ? `${d.packingConfigurations.length} configuratie(s)`
@@ -508,6 +514,8 @@ function StepBody(p) {
508
514
  return _jsx(StepKlanten, { ...p });
509
515
  case "fotos":
510
516
  return _jsx(StepFotos, { ...p });
517
+ case "warehouse":
518
+ return _jsx(StepWarehouse, { ...p });
511
519
  case "beladingen":
512
520
  return _jsx(StepBeladingen, { ...p });
513
521
  case "verpakking":
@@ -1028,6 +1036,39 @@ function StepFotos({ data, update, activeId }) {
1028
1036
  e.target.value = "";
1029
1037
  } })] }), data.images.length > 0 && (_jsx("div", { className: "grid grid-cols-4 gap-2", children: data.images.map((img, idx) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border", children: [_jsx("img", { src: img.previewUrl || img.url, alt: img.name, className: "h-24 w-full object-cover" }), _jsx("button", { type: "button", onClick: () => update("images", data.images.filter((_, i) => i !== idx)), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1 opacity-0 group-hover:opacity-100", children: _jsx(X, { className: "h-3 w-3" }) })] }, img.url))) }))] }));
1030
1038
  }
1039
+ /**
1040
+ * Warehouse-stap — verplicht in Floriday Suppliers API 2026v1.
1041
+ * Toont een select gevuld uit `floriday_warehouses_cache`. Beheer van
1042
+ * warehouses (aanmaken/bewerken) gebeurt in Instellingen → Warehouses.
1043
+ */
1044
+ function StepWarehouse({ data, update, activeId }) {
1045
+ const [warehouses, setWarehouses] = useState([]);
1046
+ const [loading, setLoading] = useState(false);
1047
+ useEffect(() => {
1048
+ if (!activeId)
1049
+ return;
1050
+ let cancelled = false;
1051
+ setLoading(true);
1052
+ supabase
1053
+ .from("floriday_warehouses_cache")
1054
+ .select("floriday_id, data")
1055
+ .eq("connection_id", activeId)
1056
+ .then(({ data: rows }) => {
1057
+ if (cancelled)
1058
+ return;
1059
+ const items = (rows ?? []).map((r) => ({
1060
+ id: String(r.floriday_id),
1061
+ name: String(r.data?.name ?? r.floriday_id),
1062
+ }));
1063
+ setWarehouses(items);
1064
+ setLoading(false);
1065
+ });
1066
+ return () => {
1067
+ cancelled = true;
1068
+ };
1069
+ }, [activeId]);
1070
+ return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Warehouse (verplicht)" }), _jsxs(Select, { value: data.warehouseId || undefined, onValueChange: (v) => update("warehouseId", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? "Laden…" : "Kies een warehouse" }) }), _jsx(SelectContent, { children: warehouses.map((w) => (_jsx(SelectItem, { value: w.id, children: w.name }, w.id))) })] })] }), warehouses.length === 0 && !loading && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Nog geen warehouses gesynchroniseerd. Ga naar", " ", _jsx(Link, { to: "/instellingen/warehouses", className: "underline", children: "Instellingen \u2192 Warehouses" }), " ", "om er \u00E9\u00E9n aan te maken."] }))] }));
1071
+ }
1031
1072
  /**
1032
1073
  * Beladingen — Floriday Suppliers API 2026v1
1033
1074
  *
@@ -1201,7 +1242,7 @@ function StepVerpakking({ data, update }) {
1201
1242
  ]);
1202
1243
  const remove = (id) => update("additionalPackaging", data.additionalPackaging.filter((p) => p.id !== id));
1203
1244
  const edit = (id, patch) => update("additionalPackaging", data.additionalPackaging.map((p) => (p.id === id ? { ...p, ...patch } : p)));
1204
- return (_jsxs("div", { className: "space-y-3", children: [data.additionalPackaging.map((p) => (_jsxs("div", { className: "grid grid-cols-[1fr_2fr_100px_auto] gap-2 items-end", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Code" }), _jsx(Input, { value: p.code, onChange: (e) => edit(p.id, { code: e.target.value }), placeholder: "bv. HOES" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Omschrijving" }), _jsx(Input, { value: p.description ?? "", onChange: (e) => edit(p.id, { description: e.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Aantal" }), _jsx(Input, { type: "number", min: 1, value: p.quantity, onChange: (e) => edit(p.id, { quantity: Number(e.target.value) }) })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: add, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Verpakkingsmateriaal toevoegen"] })] }));
1245
+ return (_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-xs text-muted-foreground bg-muted/40 rounded-md p-2 border", children: "Bestaat je artikel uit een hoes, steker of andere toegevoegde waarde? Voeg deze items lokaal toe voor exportdocumentatie. Floriday Suppliers API 2026v1 kent hiervoor g\u00E9\u00E9n veld op TradeItem \u2014 deze data blijft binnen deze module." }), data.additionalPackaging.map((p) => (_jsxs("div", { className: "grid grid-cols-[1fr_2fr_100px_auto] gap-2 items-end", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Code" }), _jsx(Input, { value: p.code, onChange: (e) => edit(p.id, { code: e.target.value }), placeholder: "bv. HOES" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Omschrijving" }), _jsx(Input, { value: p.description ?? "", onChange: (e) => edit(p.id, { description: e.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Aantal" }), _jsx(Input, { type: "number", min: 1, value: p.quantity, onChange: (e) => edit(p.id, { quantity: Number(e.target.value) }) })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, p.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: add, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Verpakkingsmateriaal toevoegen"] })] }));
1205
1246
  }
1206
1247
  function StepBeschikbaarheid({ data, update }) {
1207
1248
  const weeks = Array.from({ length: 52 }, (_, i) => i + 1);
@@ -1,6 +1,7 @@
1
1
  import type { TradeItem } from "../../lib/floriday-client";
2
- export declare function ExcelExportButton({ items, filename, }: {
2
+ export declare function ExcelExportButton({ items, filename, onExported, }: {
3
3
  items: TradeItem[];
4
4
  filename?: string;
5
+ onExported?: () => void | Promise<void>;
5
6
  }): import("react/jsx-runtime").JSX.Element;
6
7
  //# sourceMappingURL=ExcelExportButton.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExcelExportButton.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ExcelExportButton.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE3D,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAqB,GACtB,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,2CAyBA"}
1
+ {"version":3,"file":"ExcelExportButton.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ExcelExportButton.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG3D,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAqB,EACrB,UAAU,GACX,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC,2CAgDA"}
@@ -1,11 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as XLSX from "xlsx";
3
- import { Button } from "@flowselections/core";
3
+ import { Button, toast } from "@flowselections/core";
4
4
  import { Download } from "lucide-react";
5
- export function ExcelExportButton({ items, filename = "voorraad", }) {
6
- const handle = () => {
7
- const rows = items.map((i) => ({
8
- Artikelnummer: i.articleNumber,
5
+ import { ensureArticleCodes } from "../../lib/ensure-article-codes";
6
+ export function ExcelExportButton({ items, filename = "voorraad", onExported, }) {
7
+ const handle = async () => {
8
+ let exportItems;
9
+ try {
10
+ exportItems = await ensureArticleCodes(items);
11
+ }
12
+ catch (e) {
13
+ toast.error(`Artikelcodes genereren mislukt: ${e.message}`);
14
+ return;
15
+ }
16
+ const missing = exportItems.filter((i) => !(i.articleNumber ?? "").trim());
17
+ if (missing.length > 0) {
18
+ toast.error(`${missing.length} artikel(en) missen nog een artikelcode; export afgebroken.`);
19
+ return;
20
+ }
21
+ const generatedCount = exportItems.filter((i, idx) => i.articleNumber !== items[idx].articleNumber).length;
22
+ const rows = exportItems.map((i) => ({
23
+ Artikelcode: i.articleNumber,
9
24
  Productnaam: i.name,
10
25
  "Liggende voorraad": i.quantity,
11
26
  Minimum: i.minQuantity,
@@ -19,6 +34,10 @@ export function ExcelExportButton({ items, filename = "voorraad", }) {
19
34
  XLSX.utils.book_append_sheet(wb, ws, "Voorraad");
20
35
  const stamp = new Date().toISOString().slice(0, 10);
21
36
  XLSX.writeFile(wb, `${filename}-${stamp}.xlsx`);
37
+ if (generatedCount > 0) {
38
+ toast.success(`${generatedCount} artikel(en) kregen automatisch een nieuwe artikelcode.`);
39
+ await onExported?.();
40
+ }
22
41
  };
23
42
  return (_jsxs(Button, { variant: "outline", onClick: handle, disabled: items.length === 0, children: [_jsx(Download, { className: "mr-2 h-4 w-4" }), "Excel export"] }));
24
43
  }
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ /** trade_item_key for this stock row (usually the Floriday id or `draft:<uuid>`). */
3
+ tradeItemKey: string | null;
4
+ /** Optional product name to power auto-linking suggestions. */
5
+ productName?: string | null;
6
+ }
7
+ /**
8
+ * Koppelt ERP-`products` rijen aan één Floriday-voorraadrij (`trade_item_key`).
9
+ * Zonder koppeling doen bestel-triggers niets voor mailbox/manual-orders.
10
+ */
11
+ export declare function ProductLinkCard({ tradeItemKey, productName }: Props): import("react/jsx-runtime").JSX.Element;
12
+ export {};
13
+ //# sourceMappingURL=ProductLinkCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductLinkCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductLinkCard.tsx"],"names":[],"mappings":"AAWA,UAAU,KAAK;IACb,qFAAqF;IACrF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AASD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,KAAK,2CA4OnE"}
@@ -0,0 +1,159 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { Button, Card, CardContent, Input, toast, } from "@flowselections/core";
4
+ import { Link2, Loader2, RefreshCw, Trash2, Wand2 } from "lucide-react";
5
+ import { supabase } from "../../integrations/supabase/client";
6
+ /**
7
+ * Koppelt ERP-`products` rijen aan één Floriday-voorraadrij (`trade_item_key`).
8
+ * Zonder koppeling doen bestel-triggers niets voor mailbox/manual-orders.
9
+ */
10
+ export function ProductLinkCard({ tradeItemKey, productName }) {
11
+ const [linked, setLinked] = useState([]);
12
+ const [candidates, setCandidates] = useState([]);
13
+ const [query, setQuery] = useState("");
14
+ const [loading, setLoading] = useState(false);
15
+ const [busy, setBusy] = useState(false);
16
+ const reload = useCallback(async () => {
17
+ if (!tradeItemKey)
18
+ return;
19
+ setLoading(true);
20
+ try {
21
+ const { data: linkedRows } = await supabase
22
+ .from("products")
23
+ .select("id, product, barcode, trade_item_key")
24
+ .eq("trade_item_key", tradeItemKey)
25
+ .order("product");
26
+ setLinked(linkedRows ?? []);
27
+ }
28
+ finally {
29
+ setLoading(false);
30
+ }
31
+ }, [tradeItemKey]);
32
+ useEffect(() => {
33
+ void reload();
34
+ }, [reload]);
35
+ useEffect(() => {
36
+ let cancelled = false;
37
+ const q = query.trim();
38
+ if (q.length < 2) {
39
+ setCandidates([]);
40
+ return;
41
+ }
42
+ (async () => {
43
+ const { data } = await supabase
44
+ .from("products")
45
+ .select("id, product, barcode, trade_item_key")
46
+ .or(`product.ilike.%${q}%,barcode.ilike.%${q}%`)
47
+ .is("trade_item_key", null)
48
+ .limit(20);
49
+ if (!cancelled)
50
+ setCandidates(data ?? []);
51
+ })();
52
+ return () => {
53
+ cancelled = true;
54
+ };
55
+ }, [query]);
56
+ const linkProduct = async (id) => {
57
+ if (!tradeItemKey)
58
+ return;
59
+ setBusy(true);
60
+ try {
61
+ const { error } = await supabase
62
+ .from("products")
63
+ .update({ trade_item_key: tradeItemKey })
64
+ .eq("id", id);
65
+ if (error)
66
+ throw error;
67
+ toast.success("Product gekoppeld");
68
+ setQuery("");
69
+ setCandidates([]);
70
+ await reload();
71
+ }
72
+ catch (e) {
73
+ toast.error(`Koppelen mislukt: ${e.message}`);
74
+ }
75
+ finally {
76
+ setBusy(false);
77
+ }
78
+ };
79
+ const unlinkProduct = async (id) => {
80
+ setBusy(true);
81
+ try {
82
+ const { error } = await supabase
83
+ .from("products")
84
+ .update({ trade_item_key: null })
85
+ .eq("id", id);
86
+ if (error)
87
+ throw error;
88
+ toast.success("Koppeling verwijderd");
89
+ await reload();
90
+ }
91
+ catch (e) {
92
+ toast.error(`Ontkoppelen mislukt: ${e.message}`);
93
+ }
94
+ finally {
95
+ setBusy(false);
96
+ }
97
+ };
98
+ const autoLinkByName = async () => {
99
+ if (!tradeItemKey || !productName)
100
+ return;
101
+ setBusy(true);
102
+ try {
103
+ const { data, error } = await supabase
104
+ .from("products")
105
+ .select("id, product")
106
+ .ilike("product", productName)
107
+ .is("trade_item_key", null);
108
+ if (error)
109
+ throw error;
110
+ const matches = data ?? [];
111
+ if (matches.length === 0) {
112
+ toast.info(`Geen niet-gekoppelde producten met naam "${productName}" gevonden.`);
113
+ return;
114
+ }
115
+ const ids = matches.map((m) => m.id);
116
+ const { error: updErr } = await supabase
117
+ .from("products")
118
+ .update({ trade_item_key: tradeItemKey })
119
+ .in("id", ids);
120
+ if (updErr)
121
+ throw updErr;
122
+ toast.success(`${ids.length} product(en) gekoppeld op naam`);
123
+ await reload();
124
+ }
125
+ catch (e) {
126
+ toast.error(`Auto-koppelen mislukt: ${e.message}`);
127
+ }
128
+ finally {
129
+ setBusy(false);
130
+ }
131
+ };
132
+ const backfillReservations = async () => {
133
+ setBusy(true);
134
+ try {
135
+ const { data, error } = await supabase.rpc("backfill_open_order_reservations");
136
+ if (error)
137
+ throw error;
138
+ const rows = data ?? [];
139
+ const n = rows[0]?.processed_items ?? 0;
140
+ toast.success(`Backfill klaar: ${n} orderregel(s) verwerkt`);
141
+ }
142
+ catch (e) {
143
+ toast.error(`Backfill mislukt: ${e.message}`);
144
+ }
145
+ finally {
146
+ setBusy(false);
147
+ }
148
+ };
149
+ const helpText = useMemo(() => {
150
+ if (!tradeItemKey) {
151
+ return "Sla het artikel eerst op om producten te kunnen koppelen.";
152
+ }
153
+ if (linked.length === 0) {
154
+ return "Nog geen ERP-product gekoppeld. Zonder koppeling worden mailbox-orders niet automatisch van de voorraad afgeschreven.";
155
+ }
156
+ return "Bestellingen op deze ERP-producten reserveren automatisch op deze voorraadrij.";
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))) }))] })] })) })] }));
159
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"StockBulkButtons.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockBulkButtons.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsD3D,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,2CA8dA"}
1
+ {"version":3,"file":"StockBulkButtons.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockBulkButtons.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoF3D,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,2CAikBA"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useMemo, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import * as XLSX from "xlsx";
4
4
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Checkbox, Label, toast, } from "@flowselections/core";
5
5
  import { Download, FileDown, Upload, Loader2 } from "lucide-react";
@@ -8,6 +8,7 @@ import { emptyWizardData } from "../../lib/floriday-payload";
8
8
  import { upsertLocalTradeItem, updateDraftMeta } from "../../hooks/useLocalTradeItems";
9
9
  import { validateDraftForPublish } from "../../lib/floriday-import-validator";
10
10
  import { floriday } from "../../lib/floriday-client";
11
+ import { ensureArticleCodes } from "../../lib/ensure-article-codes";
11
12
  const COLUMNS = [
12
13
  "Artikelcode",
13
14
  "Artikelnaam",
@@ -20,8 +21,17 @@ const COLUMNS = [
20
21
  "Gereserveerde voorraad",
21
22
  "Inkomende voorraad",
22
23
  ];
24
+ /**
25
+ * Determine which trade_item_stock key to write to. The stock table is keyed
26
+ * by whatever `products.trade_item_key` (or the draft/floriday id) resolved
27
+ * to when the row was created — never by the synthetic UI id `erp:<uuid>`.
28
+ */
23
29
  function toKey(item) {
24
- return item.localId ? `draft:${item.localId}` : item.id;
30
+ if (item.tradeItemKey)
31
+ return item.tradeItemKey;
32
+ if (item.localId)
33
+ return `draft:${item.localId}`;
34
+ return item.id;
25
35
  }
26
36
  function parseNumber(val) {
27
37
  if (val === undefined || val === null || val === "")
@@ -49,6 +59,20 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
49
59
  const allowNegative = true;
50
60
  const [autoPublish, setAutoPublish] = useState(false);
51
61
  const [result, setResult] = useState(null);
62
+ const [pastRuns, setPastRuns] = useState([]);
63
+ const loadPastRuns = useCallback(async () => {
64
+ const { data, error } = await supabase
65
+ .from("stock_import_runs")
66
+ .select("id,filename,total_rows,updated_rows,created_rows,skipped_rows,created_at,errors,mutations")
67
+ .order("created_at", { ascending: false })
68
+ .limit(10);
69
+ if (!error)
70
+ setPastRuns((data ?? []));
71
+ }, []);
72
+ useEffect(() => {
73
+ if (open)
74
+ void loadPastRuns();
75
+ }, [open, loadPastRuns]);
52
76
  const byCode = useMemo(() => {
53
77
  const map = new Map();
54
78
  for (const it of items) {
@@ -73,11 +97,25 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
73
97
  }
74
98
  return map;
75
99
  }, [items]);
76
- const downloadTemplate = () => {
77
- const rows = items.map((i) => {
100
+ const downloadTemplate = async () => {
101
+ let exportItems;
102
+ try {
103
+ exportItems = await ensureArticleCodes(items);
104
+ }
105
+ catch (e) {
106
+ toast.error(`Artikelcodes genereren mislukt: ${e.message}`);
107
+ return;
108
+ }
109
+ const missing = exportItems.filter((i) => !(i.articleNumber ?? "").trim());
110
+ if (missing.length > 0) {
111
+ toast.error(`${missing.length} artikel(en) missen nog een artikelcode; export afgebroken.`);
112
+ return;
113
+ }
114
+ const generatedCount = exportItems.filter((i, idx) => i.articleNumber !== items[idx].articleNumber).length;
115
+ const rows = exportItems.map((i) => {
78
116
  const d = i.raw?.data;
79
117
  return {
80
- Artikelcode: i.articleNumber ?? "",
118
+ Artikelcode: i.articleNumber,
81
119
  Artikelnaam: i.name,
82
120
  "VBN-code": d?.vbnCode ?? i.raw?.vbnProductCode ?? "",
83
121
  "Botanische naam": d?.botanicalName ?? "",
@@ -97,6 +135,10 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
97
135
  XLSX.utils.book_append_sheet(wb, ws, "Voorraad");
98
136
  const stamp = new Date().toISOString().slice(0, 10);
99
137
  XLSX.writeFile(wb, `voorraad-template-${stamp}.xlsx`);
138
+ if (generatedCount > 0) {
139
+ toast.success(`${generatedCount} artikel(en) kregen automatisch een nieuwe artikelcode.`);
140
+ await onImported();
141
+ }
100
142
  };
101
143
  const downloadErrors = () => {
102
144
  if (!result?.errors.length)
@@ -121,6 +163,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
121
163
  setProgress(0);
122
164
  setResult(null);
123
165
  const errors = [];
166
+ const mutations = [];
124
167
  let updated = 0;
125
168
  let created = 0;
126
169
  let linked = 0;
@@ -205,6 +248,14 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
205
248
  trade_item_key: toKey(existing),
206
249
  ...applied,
207
250
  });
251
+ mutations.push({
252
+ articleCode: code,
253
+ articleName: existing.name,
254
+ oldPhysical: existing.quantity,
255
+ newPhysical: applied.physical_stock,
256
+ oldReserved: existing.reservedQuantity,
257
+ newReserved: applied.reserved_stock,
258
+ });
208
259
  updated++;
209
260
  setProgress(Math.round(((idx + 1) / rows.length) * 100));
210
261
  continue;
@@ -325,7 +376,37 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
325
376
  });
326
377
  }
327
378
  }
328
- setResult({ updated, created, linked, published, waiting, errors });
379
+ const finalResult = {
380
+ updated,
381
+ created,
382
+ linked,
383
+ published,
384
+ waiting,
385
+ errors,
386
+ mutations,
387
+ };
388
+ setResult(finalResult);
389
+ // Persisteer het importverslag zodat het achteraf herbekeken kan worden.
390
+ try {
391
+ const { error: logErr } = await supabase.from("stock_import_runs").insert({
392
+ user_id: userId,
393
+ connection_id: activeConnectionId,
394
+ filename: file.name,
395
+ total_rows: rows.length,
396
+ updated_rows: updated,
397
+ created_rows: created,
398
+ skipped_rows: errors.length,
399
+ errors: errors,
400
+ mutations: mutations,
401
+ });
402
+ if (logErr)
403
+ console.warn("Import log opslaan mislukt", logErr);
404
+ else
405
+ void loadPastRuns();
406
+ }
407
+ catch (logE) {
408
+ console.warn("Import log opslaan mislukt", logE);
409
+ }
329
410
  if (updated + created > 0) {
330
411
  toast.success(`${updated} bijgewerkt · ${created} nieuw`);
331
412
  await onImported();
@@ -349,7 +430,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
349
430
  const f = e.target.files?.[0];
350
431
  if (f)
351
432
  void handleFile(f);
352
- } }), importing && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "h-2 w-full overflow-hidden rounded bg-muted", children: _jsx("div", { className: "h-full bg-primary transition-all", style: { width: `${progress}%` } }) }), _jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), "Bezig met importeren\u2026 ", progress, "%"] })] })), result && (_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm space-y-2", children: [_jsx("div", { className: "font-medium", children: "Importoverzicht" }), _jsxs("ul", { className: "text-xs space-y-1", children: [_jsxs("li", { children: ["Bestaande artikelen bijgewerkt:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.updated })] }), _jsxs("li", { children: ["Nieuwe artikelen aangemaakt:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.created })] }), _jsxs("li", { children: ["Automatisch gekoppeld met Floriday:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.linked })] }), _jsxs("li", { children: ["Gepubliceerd naar Floriday:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.published })] }), _jsxs("li", { children: ["Wacht op Floriday-publicatie (gegevens incompleet):", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.waiting })] }), _jsxs("li", { children: ["Fouten:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.errors.length })] })] }), result.errors.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "max-h-40 overflow-y-auto rounded border bg-background p-2 text-xs space-y-1", children: [result.errors.slice(0, 20).map((e, i) => (_jsxs("div", { className: "text-destructive", children: ["Regel ", e.row, " \u2014 ", e.articleCode || "(geen code)", " \u2014", " ", e.reden] }, i))), result.errors.length > 20 && (_jsxs("div", { className: "text-muted-foreground", children: ["\u2026 en ", result.errors.length - 20, " meer. Download het foutbestand voor de volledige lijst."] }))] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: downloadErrors, children: [_jsx(Download, { className: "mr-2 h-4 w-4" }), "Download foutbestand"] })] }))] }))] }), _jsx(DialogFooter, { children: _jsx(Button, { variant: "ghost", onClick: () => {
433
+ } }), importing && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "h-2 w-full overflow-hidden rounded bg-muted", children: _jsx("div", { className: "h-full bg-primary transition-all", style: { width: `${progress}%` } }) }), _jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), "Bezig met importeren\u2026 ", progress, "%"] })] })), result && (_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm space-y-2", children: [_jsx("div", { className: "font-medium", children: "Importoverzicht" }), _jsxs("ul", { className: "text-xs space-y-1", children: [_jsxs("li", { children: ["Bestaande artikelen bijgewerkt:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.updated })] }), _jsxs("li", { children: ["Nieuwe artikelen aangemaakt:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.created })] }), _jsxs("li", { children: ["Automatisch gekoppeld met Floriday:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.linked })] }), _jsxs("li", { children: ["Gepubliceerd naar Floriday:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.published })] }), _jsxs("li", { children: ["Wacht op Floriday-publicatie (gegevens incompleet):", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.waiting })] }), _jsxs("li", { children: ["Fouten:", " ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: result.errors.length })] })] }), result.errors.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "max-h-40 overflow-y-auto rounded border bg-background p-2 text-xs space-y-1", children: [result.errors.slice(0, 20).map((e, i) => (_jsxs("div", { className: "text-destructive", children: ["Regel ", e.row, " \u2014 ", e.articleCode || "(geen code)", " \u2014", " ", e.reden] }, i))), result.errors.length > 20 && (_jsxs("div", { className: "text-muted-foreground", children: ["\u2026 en ", result.errors.length - 20, " meer. Download het foutbestand voor de volledige lijst."] }))] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: downloadErrors, children: [_jsx(Download, { className: "mr-2 h-4 w-4" }), "Download foutbestand"] })] }))] })), !importing && pastRuns.length > 0 && (_jsxs("div", { className: "rounded-md border bg-background p-3 text-sm space-y-2", children: [_jsx("div", { className: "font-medium", children: "Recente imports" }), _jsx("ul", { className: "text-xs space-y-1 max-h-48 overflow-y-auto", children: pastRuns.map((r) => (_jsxs("li", { className: "flex justify-between gap-2 border-b last:border-0 py-1", children: [_jsxs("span", { className: "truncate", children: [_jsx("span", { className: "text-muted-foreground", children: new Date(r.created_at).toLocaleString("nl-NL") }), " ", "\u2014 ", r.filename ?? "(zonder naam)"] }), _jsxs("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: [r.updated_rows, "\u21BB \u00B7 ", r.created_rows, "+ \u00B7 ", r.skipped_rows, "\u2715"] })] }, r.id))) })] }))] }), _jsx(DialogFooter, { children: _jsx(Button, { variant: "ghost", onClick: () => {
353
434
  setOpen(false);
354
435
  setResult(null);
355
436
  }, disabled: importing, children: "Sluiten" }) })] }) })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"useTradeItemSpec.d.ts","sourceRoot":"","sources":["../../src/hooks/useTradeItemSpec.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,eAAe,CAAC;AAEzE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,aAAa,EAAE,uBAAuB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,kBAAkB,EAAE,CAAC;IACtC,2DAA2D;IAC3D,2BAA2B,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,0FAA0F;IAC1F,2BAA2B,EAAE,OAAO,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mGAAmG;IACnG,0BAA0B,EAAE,OAAO,CAAC;IACpC,4FAA4F;IAC5F,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,uBAAuB,EAAE,OAAO,CAAC;CAClC;AA8bD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAC/C,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,YAAY,GAAG,IAAI,EACjC,kBAAkB,CAAC,EAAE,sBAAsB,EAAE,GAAG,IAAI,GACnD,aAAa,CAmEf"}
1
+ {"version":3,"file":"useTradeItemSpec.d.ts","sourceRoot":"","sources":["../../src/hooks/useTradeItemSpec.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,eAAe,CAAC;AAEzE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,aAAa,EAAE,uBAAuB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,kBAAkB,EAAE,CAAC;IACtC,2DAA2D;IAC3D,2BAA2B,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,0FAA0F;IAC1F,2BAA2B,EAAE,OAAO,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mGAAmG;IACnG,0BAA0B,EAAE,OAAO,CAAC;IACpC,4FAA4F;IAC5F,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,uBAAuB,EAAE,OAAO,CAAC;CAClC;AA8bD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAC/C,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,YAAY,GAAG,IAAI,EACjC,kBAAkB,CAAC,EAAE,sBAAsB,EAAE,GAAG,IAAI,GACnD,aAAa,CAsEf"}
@@ -402,7 +402,10 @@ export function useTradeItemSpec(schema, vbnCode, productGroup, regulatoryFeatur
402
402
  fromRegulatory = regulatoryFeatures.map((f) => ({
403
403
  code: f.code,
404
404
  label: f.label,
405
- required: f.regulation === "required",
405
+ // Floriday's server-side validator rejeert ook "advised" karakteristieken
406
+ // (bv. S62) met "Characteristic X is required". Behandel required+advised
407
+ // beide als verplicht om stille 400's te voorkomen.
408
+ required: f.regulation === "required" || f.regulation === "advised",
406
409
  listName: f.code,
407
410
  allowedValues: f.values,
408
411
  }));
@@ -1,9 +1,10 @@
1
1
  import { type TradeItem } from "../lib/floriday-client";
2
2
  /**
3
- * Reads trade items from the shared `floriday_trade_items_cache` (gepubliceerd,
4
- * gevuld door de Auth-module elke 5 min) en merget lokale, niet-gepubliceerde
5
- * producten uit `trade_item_drafts`. Voorraadwaarden uit `trade_item_stock`
6
- * overschrijven de cache-waarden (lokale bron van waarheid voor stock).
3
+ * Reads trade items from three sources and merges them into one voorraad list:
4
+ * 1. `products` rows tied to a `product:<id>` key (pure ERP items).
5
+ * 2. `trade_item_drafts` (lokaal concept, mogelijk in publiceerflow).
6
+ * 3. `floriday_trade_items_cache` (gepubliceerd artikel).
7
+ * Voorraadwaarden komen uit `trade_item_stock` en overschrijven cache-defaults.
7
8
  */
8
9
  export declare function useVoorraadData(connectionId: string | null): {
9
10
  items: TradeItem[];
@@ -1 +1 @@
1
- {"version":3,"file":"useVoorraadData.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoorraadData.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAoB5E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;;EAkE1D"}
1
+ {"version":3,"file":"useVoorraadData.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoorraadData.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAkD5E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;;EA6E1D"}