@flowselections/floriday-voorraad 1.0.145 → 1.0.147

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 (72) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts +3 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  3. package/dist-lib/components/voorraad/ArtikelWizard.js +56 -52
  4. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
  5. package/dist-lib/components/voorraad/EditArtikelPage.js +3 -4
  6. package/dist-lib/components/voorraad/ProductCustomFieldsCard.d.ts.map +1 -1
  7. package/dist-lib/components/voorraad/ProductCustomFieldsCard.js +29 -13
  8. package/dist-lib/components/voorraad/ProductSalePriceField.d.ts.map +1 -1
  9. package/dist-lib/components/voorraad/ProductSalePriceField.js +9 -8
  10. package/dist-lib/components/voorraad/StepSamenstelling.d.ts.map +1 -1
  11. package/dist-lib/components/voorraad/StepSamenstelling.js +35 -10
  12. package/dist-lib/components/voorraad/TrayStickerCard.d.ts +5 -1
  13. package/dist-lib/components/voorraad/TrayStickerCard.d.ts.map +1 -1
  14. package/dist-lib/components/voorraad/TrayStickerCard.js +64 -66
  15. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +0 -5
  16. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
  17. package/dist-lib/components/voorraad/ValidationOverviewTable.js +10 -4
  18. package/dist-lib/components/voorraad/VoorraadPage.d.ts +1 -13
  19. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  20. package/dist-lib/components/voorraad/VoorraadPage.js +102 -140
  21. package/dist-lib/components/voorraad/VoorraadSturingCard.d.ts +6 -0
  22. package/dist-lib/components/voorraad/VoorraadSturingCard.d.ts.map +1 -0
  23. package/dist-lib/components/voorraad/VoorraadSturingCard.js +47 -0
  24. package/dist-lib/components/voorraad/artikel/ArtikelPagina.d.ts +5 -0
  25. package/dist-lib/components/voorraad/artikel/ArtikelPagina.d.ts.map +1 -0
  26. package/dist-lib/components/voorraad/artikel/ArtikelPagina.js +263 -0
  27. package/dist-lib/components/voorraad/artikel/TwijfelKiezen.d.ts +7 -0
  28. package/dist-lib/components/voorraad/artikel/TwijfelKiezen.d.ts.map +1 -0
  29. package/dist-lib/components/voorraad/artikel/TwijfelKiezen.js +54 -0
  30. package/dist-lib/components/voorraad/artikel/opslaan-context.d.ts +21 -0
  31. package/dist-lib/components/voorraad/artikel/opslaan-context.d.ts.map +1 -0
  32. package/dist-lib/components/voorraad/artikel/opslaan-context.js +15 -0
  33. package/dist-lib/components/voorraad/lijst/ImporteerKnop.d.ts +5 -0
  34. package/dist-lib/components/voorraad/lijst/ImporteerKnop.d.ts.map +1 -0
  35. package/dist-lib/components/voorraad/lijst/ImporteerKnop.js +20 -0
  36. package/dist-lib/components/voorraad/lijst/LijstExportKnop.d.ts +6 -0
  37. package/dist-lib/components/voorraad/lijst/LijstExportKnop.d.ts.map +1 -0
  38. package/dist-lib/components/voorraad/lijst/LijstExportKnop.js +43 -0
  39. package/dist-lib/components/voorraad/lijst/SnelActieDialogen.d.ts +16 -0
  40. package/dist-lib/components/voorraad/lijst/SnelActieDialogen.d.ts.map +1 -0
  41. package/dist-lib/components/voorraad/lijst/SnelActieDialogen.js +95 -0
  42. package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.d.ts +21 -0
  43. package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.d.ts.map +1 -0
  44. package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.js +38 -0
  45. package/dist-lib/hooks/useVoorraadLijst.d.ts +21 -0
  46. package/dist-lib/hooks/useVoorraadLijst.d.ts.map +1 -0
  47. package/dist-lib/hooks/useVoorraadLijst.js +71 -0
  48. package/dist-lib/index.d.ts.map +1 -1
  49. package/dist-lib/index.js +2 -0
  50. package/dist-lib/integrations/supabase/types.d.ts +465 -1
  51. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  52. package/dist-lib/lib/artikel-pagina.d.ts +57 -0
  53. package/dist-lib/lib/artikel-pagina.d.ts.map +1 -0
  54. package/dist-lib/lib/artikel-pagina.js +93 -0
  55. package/dist-lib/lib/count-stock.d.ts +17 -0
  56. package/dist-lib/lib/count-stock.d.ts.map +1 -0
  57. package/dist-lib/lib/count-stock.js +26 -0
  58. package/dist-lib/lib/floriday-import-validator.d.ts +9 -0
  59. package/dist-lib/lib/floriday-import-validator.d.ts.map +1 -1
  60. package/dist-lib/lib/floriday-import-validator.js +13 -0
  61. package/dist-lib/lib/pending-photos.d.ts +20 -0
  62. package/dist-lib/lib/pending-photos.d.ts.map +1 -0
  63. package/dist-lib/lib/pending-photos.js +90 -0
  64. package/dist-lib/lib/voorraad-custom-fields.d.ts +16 -1
  65. package/dist-lib/lib/voorraad-custom-fields.d.ts.map +1 -1
  66. package/dist-lib/lib/voorraad-custom-fields.js +12 -6
  67. package/dist-lib/lib/voorraad-lijst.d.ts +91 -0
  68. package/dist-lib/lib/voorraad-lijst.d.ts.map +1 -0
  69. package/dist-lib/lib/voorraad-lijst.js +82 -0
  70. package/package.json +1 -1
  71. package/sql/AGENTS.md +4 -0
  72. package/sql/floriday-voorraad-schema.sql +723 -10
@@ -9,7 +9,9 @@ export interface WizardInitial {
9
9
  }
10
10
  /** Neemt naam, code, voorraad, prijs en (indien compleet) belading over. */
11
11
  export declare function applyWizardInitial(base: WizardData, init?: WizardInitial): WizardData;
12
- export declare function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, resumeJobId, initial, }?: {
12
+ export declare function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, resumeJobId, initial, embedded, }?: {
13
+ /** Binnen de artikelpagina: voorraad, samenstelling en geschiedenis staan in de eigen tabbladen. */
14
+ embedded?: boolean;
13
15
  draftId?: string;
14
16
  tradeItemId?: string;
15
17
  productId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA2DA,OAAO,EAUL,KAAK,UAAU,EAMhB,MAAM,4BAA4B,CAAC;AA4DpC,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,UAAU,CA0BrF;AAiED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,GACR,GAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,aAAa,CAAC;CACpB,+BAy4EL"}
1
+ {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA6DA,OAAO,EAUL,KAAK,UAAU,EAMhB,MAAM,4BAA4B,CAAC;AA4DpC,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,UAAU,CA0BrF;AAgED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,EACP,QAAgB,GACjB,GAAE;IACD,oGAAoG;IACpG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,aAAa,CAAC;CACpB,+BA+6EL"}
@@ -3,9 +3,10 @@ import { saveOwnPackings, packingRowsFromWizard } from "../../lib/product-packin
3
3
  import { FloridayLoadCarriers } from "./FloridayLoadCarriers.js";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { Link, useNavigate } from "@tanstack/react-router";
6
- import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button, Card, CardContent, Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, toast, } from "@flowselections/core";
6
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button, Card, CardContent, Checkbox, Dialog, DialogDescription, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, toast, } from "@flowselections/core";
7
7
  import { ArrowLeft, ArrowRight, Check, ChevronDown, ClipboardList, Info, Loader2, Plus, Search, Trash2, Upload, UploadCloud, Users, X, Star, } from "lucide-react";
8
8
  import { useFloridayConnection } from "../../hooks/useFloridayConnection.js";
9
+ import { countPendingPhotos, registerPendingPhoto, resolvePendingPhotos, wijzigingenOverzicht } from "../../lib/pending-photos.js";
9
10
  import { useFlorydayCustomers } from "../../hooks/useFlorydayCustomers.js";
10
11
  import { floriday } from "../../lib/floriday-client.js";
11
12
  import { listFloricode, listFloricodeByField } from "../../lib/floricode-client.js";
@@ -91,7 +92,6 @@ const STEPS = [
91
92
  { key: "fotos", label: "Foto's" },
92
93
  { key: "warehouse", label: "Warehouse" },
93
94
  { key: "beladingen", label: "Beladingen" },
94
- { key: "verpakking", label: "Aanvullende verpakkingsmaterialen" },
95
95
  { key: "samenstelling", label: "Samenstelling" },
96
96
  { key: "beschikbaarheid", label: "Verwachte beschikbaarheid" },
97
97
  { key: "aanbieden", label: "Aanbieden & voorraad" },
@@ -137,7 +137,7 @@ async function lookupProductGroupForVbn(vbnCode) {
137
137
  .maybeSingle();
138
138
  return data ? groupOfRecord(data) : null;
139
139
  }
140
- export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, resumeJobId, initial, } = {}) {
140
+ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId, resumeJobId, initial, embedded = false, } = {}) {
141
141
  const navigate = useNavigate();
142
142
  const { activeId, activeConnection, loading: connLoading } = useFloridayConnection();
143
143
  const connectionReadiness = useQuery({
@@ -960,7 +960,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
960
960
  const update = (key, value) => setData((d) => ({ ...d, [key]: value }));
961
961
  // Stap 1 (VBN) is de poort: zodra die geldig is, mag de gebruiker vrij
962
962
  // tussen alle stappen springen door erop te klikken.
963
- const firstStepUnlocked = useMemo(() => steps.length > 0 && validateStepLocal(steps[0].key, data).ok, [steps, data]);
963
+ const firstStepUnlocked = useMemo(() => (!isEditingPublished && !publishToFloriday) || (steps.length > 0 && validateStepLocal(steps[0].key, data).ok), [steps, data, isEditingPublished, publishToFloriday]);
964
964
  // Per-stap validatie → bron voor visuele status én publish-gate.
965
965
  // "samenstelling" is read-only ("binnenkort beschikbaar") en mag nooit
966
966
  // als ongeldig tellen.
@@ -971,7 +971,12 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
971
971
  }), [steps, data, spec, regulatoryQuery.isLoading, regulatoryQuery.isError, regulatoryQuery.error]);
972
972
  // Foto's zijn alleen verplicht om naar Floriday te publiceren
973
973
  // (Floriday: "One or more product images"); opslaan als concept kan zonder.
974
- const gateValidity = stepValidity.map((v, i) => v || (steps[i]?.key === "fotos" && !publishToFloriday));
974
+ // 1.0.146: zonder publiceren telt alleen wat lokaal nodig is (een naam).
975
+ // Floriday-velden en foto's zijn dan nooit "nog nodig".
976
+ const lokaalAlleen = !isEditingPublished && !publishToFloriday;
977
+ const gateValidity = lokaalAlleen
978
+ ? steps.map((st) => st.key !== "artikel" || data.name.trim().length > 0)
979
+ : stepValidity;
975
980
  const invalidStepCount = gateValidity.filter((v) => !v).length;
976
981
  const firstInvalidIndex = gateValidity.findIndex((v) => !v);
977
982
  const goTo = (i) => {
@@ -1197,6 +1202,16 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1197
1202
  });
1198
1203
  });
1199
1204
  }, []);
1205
+ const wizardData = data;
1206
+ const [overzicht, setOverzicht] = useState(null);
1207
+ /** Niets gaat naar Floriday zonder dit overzicht vooraf. */
1208
+ const metOverzicht = (run, naarFloriday) => {
1209
+ if (!naarFloriday) {
1210
+ run();
1211
+ return;
1212
+ }
1213
+ setOverzicht({ wijzigingen: wijzigingenOverzicht(lastSavedRef.current, data), fotos: countPendingPhotos(data), run });
1214
+ };
1200
1215
  const submit = async (options) => {
1201
1216
  if (!activeId) {
1202
1217
  toast.error("Geen actieve verbinding met de handelsomgeving", {
@@ -1204,6 +1219,18 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1204
1219
  });
1205
1220
  return;
1206
1221
  }
1222
+ // Foto's gaan pas nu naar Floriday (na het overzicht vooraf).
1223
+ let data = wizardData;
1224
+ if (wantsPublish && countPendingPhotos(wizardData) > 0) {
1225
+ try {
1226
+ data = await resolvePendingPhotos(wizardData, (f) => floriday.uploadImage(activeId, f, f.type || "image/jpeg", f.name));
1227
+ setData(data);
1228
+ }
1229
+ catch (e) {
1230
+ toast.error(e.message || "Foto's konden niet naar Floriday");
1231
+ return;
1232
+ }
1233
+ }
1207
1234
  // Zijn de productregels (schema) nog niet binnen? Dan publiceren we niet,
1208
1235
  // maar bewaren we de wijziging ALTIJD lokaal en markeren we hem als
1209
1236
  // "nog niet doorgegeven aan Floriday".
@@ -1820,6 +1847,8 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1820
1847
  // 2. Doorgeven aan Floriday.
1821
1848
  try {
1822
1849
  await saveWarehousePreference(pushTargetId, snapshot.warehouseId);
1850
+ snapshot = await resolvePendingPhotos(snapshot, (f) => floriday.uploadImage(activeId, f, f.type || "image/jpeg", f.name));
1851
+ setData(snapshot);
1823
1852
  const payload = buildUpdateTradeItemPayload(snapshot);
1824
1853
  const res = await floriday.updateTradeItem(activeId, pushTargetId, payload);
1825
1854
  if (!res?.attemptId) {
@@ -1872,16 +1901,8 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1872
1901
  lastSavedRef.current = serialized;
1873
1902
  return;
1874
1903
  }
1875
- if (lastSavedRef.current === serialized)
1876
- return;
1877
- const snapshot = data;
1878
- const t = window.setTimeout(() => {
1879
- // De lokale opslag slaagt (bijna) altijd; de baseline mag dus mee, ook als
1880
- // de Floriday-push faalt — de invoer staat dan veilig in de eigen database.
1881
- lastSavedRef.current = serialized;
1882
- void persistAndPush(snapshot);
1883
- }, 800);
1884
- return () => window.clearTimeout(t);
1904
+ // 1.0.146: geen automatisch opslaan/versturen meer. Alleen bewust via
1905
+ // "Naar Floriday sturen" / "Op Floriday zetten" met het overzicht vooraf.
1885
1906
  }, [
1886
1907
  data,
1887
1908
  draftId,
@@ -1910,7 +1931,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1910
1931
  if (connLoading || draftLoading || publishedLoading || productLoading) {
1911
1932
  // Voorraadblok direct bruikbaar; Floriday-gegevens laden op de achtergrond (1.0.138).
1912
1933
  const vroegVoorraad = !connLoading && (tradeItemId || productId || draftId);
1913
- return (_jsxs("main", { className: "p-6 space-y-4", children: [vroegVoorraad && (_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: productId ?? null })), _jsxs(Card, { className: "p-6 flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), publishedLoading
1934
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [vroegVoorraad && !embedded && (_jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: productId ?? null })), _jsxs(Card, { className: "p-6 flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), publishedLoading
1914
1935
  ? "Artikel laden..."
1915
1936
  : draftLoading
1916
1937
  ? "Lokaal artikel laden..."
@@ -1925,7 +1946,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1925
1946
  setDeleteOpen(o);
1926
1947
  if (!o)
1927
1948
  setDeleteAck(false);
1928
- }, children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Product verwijderen?" }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { children: ["Je staat op het punt om ", _jsx("strong", { children: data.name || "dit artikel" }), data.articleCode ? _jsxs(_Fragment, { children: [" (artikelnummer ", _jsx("code", { children: data.articleCode }), ")"] }) : null, " ", "lokaal uit de voorraad te verwijderen."] }), _jsxs("div", { className: "rounded-md border p-3 bg-muted/40 space-y-1", children: [_jsxs("div", { children: ["Huidige voorraad: ", _jsx("strong", { children: productMeta.physicalStock }), " stuks"] }), _jsxs("div", { children: ["Floriday-koppeling:", " ", productMeta.floridayActive || productMeta.floridayTradeItemId ? (_jsx("span", { className: "text-emerald-700 font-medium", children: "actief" })) : (_jsx("span", { className: "text-muted-foreground", children: "geen" }))] })] }), (productMeta.floridayActive || productMeta.floridayTradeItemId) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900", children: [_jsx("strong", { children: "Let op:" }), " dit artikel is gekoppeld met Floriday. Als je alleen de lokale kant verwijdert, blijft het artikel op Floriday bestaan en verschijnt het bij de volgende sync weer terug in de lijst. Trek het artikel daarna zelf in via het Floriday-portaal als je het echt weg wilt."] })), productMeta.physicalStock > 0 && (_jsxs("div", { className: "rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-900", children: ["Er staat nog voorraad (", productMeta.physicalStock, " stuks) op dit product. Deze voorraad wordt \u00F3\u00F3k verwijderd en kan niet automatisch worden hersteld."] })), _jsxs("label", { className: "flex items-start gap-2 pt-1", children: [_jsx(Checkbox, { checked: deleteAck, onCheckedChange: (v) => setDeleteAck(Boolean(v)) }), _jsx("span", { children: "Ik weet zeker dat ik dit product wil verwijderen en begrijp de gevolgen." })] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { disabled: !deleteAck, onClick: handleDeleteDraft, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Definitief verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, productId: productId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), pendingSync && (_jsxs(Card, { className: "p-4 border-amber-300 bg-amber-50 text-amber-900 space-y-2", children: [_jsx("div", { className: "font-medium text-sm", children: "Je wijzigingen zijn opgeslagen, maar nog niet doorgegeven aan Floriday" }), _jsxs("div", { className: "text-xs", children: ["Alles wat je hier hebt ingevuld blijft bewaard in dit systeem. Zodra het doorgeven lukt, verdwijnt deze melding vanzelf.", pendingSync.error ? ` Reden: ${pendingSync.error}` : ""] }), _jsx(FloridayStockReasonLine, { productId: linkedProductId ?? productId ?? null }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: retryingSync, onClick: retrySync, children: retryingSync ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin mr-1" }), " Bezig\u2026"] })) : ("Opnieuw proberen") })] })), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [editTarget.text, " Je kunt vrij tussen alle secties navigeren."] })] }), _jsxs(Button, { onClick: () => void submit(), disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Wijzigingen opslaan"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Lokaal artikel bewerken" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Niet gepubliceerd. Je kunt vrij tussen alle secties navigeren." })] }), _jsxs(Button, { onClick: () => void submit(), disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), publishToFloriday ? "Publiceren naar Floriday" : "Wijzigingen opslaan"] })] })) })), isEditing && (_jsxs(_Fragment, { children: [_jsx(ProductStructureCard, { productId: linkedProductId ?? productId ?? null }), _jsx(ArtikelCompositionCard, { productId: linkedProductId ?? productId ?? null, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1949
+ }, children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Product verwijderen?" }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { children: ["Je staat op het punt om ", _jsx("strong", { children: data.name || "dit artikel" }), data.articleCode ? _jsxs(_Fragment, { children: [" (artikelnummer ", _jsx("code", { children: data.articleCode }), ")"] }) : null, " ", "lokaal uit de voorraad te verwijderen."] }), _jsxs("div", { className: "rounded-md border p-3 bg-muted/40 space-y-1", children: [_jsxs("div", { children: ["Huidige voorraad: ", _jsx("strong", { children: productMeta.physicalStock }), " stuks"] }), _jsxs("div", { children: ["Floriday-koppeling:", " ", productMeta.floridayActive || productMeta.floridayTradeItemId ? (_jsx("span", { className: "text-emerald-700 font-medium", children: "actief" })) : (_jsx("span", { className: "text-muted-foreground", children: "geen" }))] })] }), (productMeta.floridayActive || productMeta.floridayTradeItemId) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900", children: [_jsx("strong", { children: "Let op:" }), " dit artikel is gekoppeld met Floriday. Als je alleen de lokale kant verwijdert, blijft het artikel op Floriday bestaan en verschijnt het bij de volgende sync weer terug in de lijst. Trek het artikel daarna zelf in via het Floriday-portaal als je het echt weg wilt."] })), productMeta.physicalStock > 0 && (_jsxs("div", { className: "rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-900", children: ["Er staat nog voorraad (", productMeta.physicalStock, " stuks) op dit product. Deze voorraad wordt \u00F3\u00F3k verwijderd en kan niet automatisch worden hersteld."] })), _jsxs("label", { className: "flex items-start gap-2 pt-1", children: [_jsx(Checkbox, { checked: deleteAck, onCheckedChange: (v) => setDeleteAck(Boolean(v)) }), _jsx("span", { children: "Ik weet zeker dat ik dit product wil verwijderen en begrijp de gevolgen." })] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { disabled: !deleteAck, onClick: handleDeleteDraft, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Definitief verwijderen" })] })] })] }))] }), isEditing && (data.name || data.vbnName) && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Artikel" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx("h1", { className: "text-2xl font-semibold leading-tight", children: data.name || data.vbnName }), (isEditingPublished || draftFloridayId) ? (syncState === "saving" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " Synchroniseren\u2026"] })) : syncState === "saved" ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700", children: [_jsx(Check, { className: "h-3 w-3" }), " Gesynchroniseerd"] })) : syncState === "error" ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700", title: syncError ?? undefined, children: "Sync mislukt \u2014 klik Opslaan om opnieuw te proberen" })) : null) : (_jsx("span", { className: "inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground", children: "Nog niet gepubliceerd naar Floriday" })), _jsx(PriceStatusBadge, { tradeItemId: tradeItemId ?? draftFloridayId ?? null, productId: productId ?? null, offerForSale: data.offerForSale })] }), data.name && data.vbnName && data.name !== data.vbnName && (_jsx("div", { className: "text-sm text-muted-foreground", children: data.vbnName }))] })), _jsx(Dialog, { open: overzicht !== null, onOpenChange: (o) => !o && setOverzicht(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Naar Floriday sturen" }), _jsx(DialogDescription, { children: "Dit gaat naar Floriday. Er verandert niets tot je bevestigt." })] }), _jsxs("div", { className: "space-y-2 text-sm", children: [overzicht && overzicht.wijzigingen.length > 0 ? (_jsx("ul", { className: "list-disc pl-5", children: overzicht.wijzigingen.map((w) => _jsx("li", { children: w }, w)) })) : (_jsx("p", { className: "text-muted-foreground", children: "Geen gewijzigde onderdelen sinds de laatste keer; de huidige gegevens worden opnieuw verstuurd." })), overzicht && overzicht.fotos > 0 && _jsxs("p", { children: [overzicht.fotos, " nieuwe foto('s) worden ge\u00FCpload."] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setOverzicht(null), children: "Annuleren" }), _jsx(Button, { onClick: () => { const r = overzicht?.run; setOverzicht(null); r?.(); }, children: "Versturen" })] })] }) }), pendingSync && (_jsxs(Card, { className: "p-4 border-amber-300 bg-amber-50 text-amber-900 space-y-2", children: [_jsx("div", { className: "font-medium text-sm", children: "Je wijzigingen zijn opgeslagen, maar nog niet doorgegeven aan Floriday" }), _jsxs("div", { className: "text-xs", children: ["Alles wat je hier hebt ingevuld blijft bewaard in dit systeem. Zodra het doorgeven lukt, verdwijnt deze melding vanzelf.", pendingSync.error ? ` Reden: ${pendingSync.error}` : ""] }), _jsx(FloridayStockReasonLine, { productId: linkedProductId ?? productId ?? null }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: retryingSync, onClick: () => metOverzicht(() => void retrySync(), true), children: retryingSync ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin mr-1" }), " Bezig\u2026"] })) : ("Opnieuw proberen") })] })), isEditing && (_jsx(Card, { className: "p-4 bg-muted/30 flex items-center gap-3 text-sm", children: isEditingPublished ? (_jsxs(_Fragment, { children: [_jsx("img", { src: floridayLogo, alt: "Floriday", className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium", children: "Gepubliceerd artikel bewerken" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [editTarget.text, " Je kunt vrij tussen alle secties navigeren."] })] }), _jsxs(Button, { onClick: () => metOverzicht(() => void submit(), wantsPublish), 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: () => metOverzicht(() => void submit(), wantsPublish), 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 && !embedded && (_jsxs(_Fragment, { children: [_jsx(ProductStructureCard, { productId: linkedProductId ?? productId ?? null }), _jsx(ArtikelCompositionCard, { productId: linkedProductId ?? productId ?? null, floridayId: tradeItemId ?? draftFloridayId ?? null }), _jsx(FloridayInfoCard, { floridayTradeItemId: tradeItemId ?? draftFloridayId ?? null, local: {
1929
1950
  name: data.name || data.vbnName,
1930
1951
  articleNumber: data.articleCode,
1931
1952
  gtin: data.gtin,
@@ -1953,7 +1974,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1953
1974
  ? "border-destructive bg-destructive text-destructive-foreground"
1954
1975
  : "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, packingStatus: packingStatus, update: update, vbnProducts: vbnProducts, featureTypes: featureTypes, packagingTypes: packagingTypes, loadCarriers: loadCarriers, floridayCustomers: floridayCustomers, schemaQuery: schemaQuery, regulatoryQuery: regulatoryQuery, spec: spec, collectionsList: collectionsList, setCollectionsList: setCollectionsList, activeId: activeId, acceptedVbnCodes: acceptedVbnCodes, priceGroups: priceGroups, parentProductId: linkedProductId ?? draftId ?? null, parentFloridayId: draftFloridayId ?? tradeItemId ?? null, articleCodeLocked: Boolean(draftFloridayId || tradeItemId), botanicalDraft: botanicalDraft, setBotanicalDraft: setBotanicalDraft }) }), 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
1955
1976
  ? "Het artikel wordt aangemaakt in Floriday en is binnen enkele minuten zichtbaar voor klanten."
1956
- : "Het artikel wordt alleen lokaal opgeslagen. Je kunt het later vanuit de voorraadlijst alsnog publiceren naar Floriday." })] }), _jsx(Switch, { id: "publish-to-floriday", checked: publishToFloriday, onCheckedChange: setPublishToFloriday })] })), (isLastStep || isEditing) && (_jsx(PublishReadinessCard, { report: readiness, loading: connLoading || connectionReadiness.isLoading || schemaQuery.isLoading, onGoToStep: goToStepKey })), publishJob && (_jsx(PublishProgressCard, { job: publishJob, steps: publishSteps, retrying: submitting, onRetry: () => void submit({ resumeJobId: publishJob.id }), onFixStep: (key) => goToStepKey(key === "trade-item" ? "eigenschappen" : "aanbod") })), (isLastStep || isEditing) && invalidStepCount > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive flex items-center justify-between gap-3", children: [_jsxs("span", { children: ["Nog ", invalidStepCount, " stap", invalidStepCount === 1 ? "" : "pen", " onvolledig"] }), firstInvalidIndex >= 0 && (_jsxs("button", { type: "button", onClick: () => setStep(firstInvalidIndex), className: "underline font-medium", children: ["Ga naar ", steps[firstInvalidIndex].label] }))] })), _jsx("div", { className: "flex justify-end pt-2", children: isLastStep || isEditing ? (_jsxs(Button, { onClick: () => void submit(), disabled: submitting || invalidStepCount > 0, children: [submitting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), isEditingPublished
1977
+ : "Het artikel wordt alleen lokaal opgeslagen. Je kunt het later vanuit de voorraadlijst alsnog publiceren naar Floriday." })] }), _jsx(Switch, { id: "publish-to-floriday", checked: publishToFloriday, onCheckedChange: setPublishToFloriday })] })), (isLastStep || isEditing) && (_jsx(PublishReadinessCard, { report: readiness, loading: connLoading || connectionReadiness.isLoading || schemaQuery.isLoading, onGoToStep: goToStepKey })), publishJob && (_jsx(PublishProgressCard, { job: publishJob, steps: publishSteps, retrying: submitting, onRetry: () => metOverzicht(() => void submit({ resumeJobId: publishJob.id }), true), onFixStep: (key) => goToStepKey(key === "trade-item" ? "eigenschappen" : "aanbod") })), (isLastStep || isEditing) && invalidStepCount > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive flex items-center justify-between gap-3", children: [_jsxs("span", { children: ["Nog ", invalidStepCount, " stap", invalidStepCount === 1 ? "" : "pen", " onvolledig"] }), firstInvalidIndex >= 0 && (_jsxs("button", { type: "button", onClick: () => setStep(firstInvalidIndex), className: "underline font-medium", children: ["Ga naar ", steps[firstInvalidIndex].label] }))] })), _jsx("div", { className: "flex justify-end pt-2", children: isLastStep || isEditing ? (_jsxs(Button, { onClick: () => metOverzicht(() => void submit(), wantsPublish), 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
1957
1978
  ? "Wijzigingen opslaan"
1958
1979
  : publishToFloriday
1959
1980
  ? isEditingDraft
@@ -1963,10 +1984,11 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1963
1984
  ? "Wijzigingen opslaan"
1964
1985
  : "Artikel aanmaken"] })) : ((() => {
1965
1986
  const pendingBotanical = s.key === "paspoort" && botanicalDraft.trim() !== "";
1966
- const canGo = isValid || s.key === "fotos" || pendingBotanical;
1987
+ const canGo = isValid || gateValidity[i] || pendingBotanical;
1967
1988
  const res = canGo ? null : validateStepLocal(s.key, data);
1968
- const reasons = res && !res.ok ? res.errors : [];
1969
- return (_jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsxs(Button, { onClick: next, disabled: !canGo, children: ["Volgende stap", _jsx(ArrowRight, { className: "ml-2 h-4 w-4" })] }), !canGo && reasons.length > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground text-right", children: ["Nog nodig: ", reasons.join(" · ")] })), s.key === "fotos" && !isValid && (_jsx("p", { className: "text-xs text-muted-foreground text-right", children: "Je kunt verder zonder foto. Voor publiceren naar Floriday is minstens \u00E9\u00E9n foto nodig." }))] }));
1989
+ // Alleen wat echt ontbreekt, zonder herhalingen.
1990
+ const reasons = res && !res.ok ? Array.from(new Set(res.errors)) : [];
1991
+ return (_jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsxs(Button, { onClick: next, disabled: !canGo, children: ["Volgende stap", _jsx(ArrowRight, { className: "ml-2 h-4 w-4" })] }), !canGo && reasons.length > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground text-right", children: ["Nog nodig: ", reasons.join(" · ")] })), s.key === "fotos" && !isValid && !lokaalAlleen && (_jsx("p", { className: "text-xs text-muted-foreground text-right", children: "Je kunt verder zonder foto. Voor publiceren naar Floriday is minstens \u00E9\u00E9n foto nodig." }))] }));
1970
1992
  })()) })] }))] }) }, s.key));
1971
1993
  }) }), _jsx(AlertDialog, { open: Boolean(conflict), onOpenChange: (open) => {
1972
1994
  if (!open)
@@ -2566,10 +2588,6 @@ function StepFotos({ data, update, activeId }) {
2566
2588
  const onFiles = async (files) => {
2567
2589
  if (!files || files.length === 0)
2568
2590
  return;
2569
- if (!activeId) {
2570
- toast.error("Geen actieve Floriday-koppeling – foto's kunnen niet worden geüpload");
2571
- return;
2572
- }
2573
2591
  const fileArr = Array.from(files);
2574
2592
  setUploading(true);
2575
2593
  setUploadError(null);
@@ -2617,27 +2635,14 @@ function StepFotos({ data, update, activeId }) {
2617
2635
  if (storageErr) {
2618
2636
  console.warn("[upload] Supabase storage mislukt", f.name, storageErr);
2619
2637
  }
2620
- // 3) Upload naar Floriday met retries (in de client).
2621
- try {
2622
- const res = await floriday.uploadImage(activeId, f, f.type || "image/jpeg", f.name);
2623
- if (!res.url)
2624
- throw new Error("Floriday upload gaf geen URL terug");
2625
- const previewUrl = URL.createObjectURL(f);
2626
- additions.push({
2627
- name: f.name,
2628
- url: res.url,
2629
- previewUrl,
2630
- imageId: res.imageId ?? null,
2631
- });
2632
- }
2633
- catch (e) {
2634
- const msg = e instanceof Error && e.message
2635
- ? e.message
2636
- : "Foto kon niet worden geüpload.";
2637
- console.error("[StepFotos] upload mislukt", { filename: f.name, message: msg, error: e });
2638
- setUploadError({ message: `${f.name}: ${msg}`, detail: e?.adminDetail ?? null });
2639
- toast.error(`${f.name} — ${msg}`);
2640
- }
2638
+ // 3) Niet naar Floriday bij het kiezen: alleen een tijdelijke verwijzing.
2639
+ // Uploaden gebeurt pas bij "Naar Floriday sturen" (na het overzicht).
2640
+ additions.push({
2641
+ name: f.name,
2642
+ url: registerPendingPhoto(f),
2643
+ previewUrl: URL.createObjectURL(f),
2644
+ imageId: null,
2645
+ });
2641
2646
  }
2642
2647
  };
2643
2648
  await Promise.all(Array.from({ length: CONCURRENCY }, worker));
@@ -2868,19 +2873,18 @@ function PackingConfigDialog({ open, onOpenChange, initial, isFirst, packagingTy
2868
2873
  (draft.source === "VBN" || Boolean(draft.packagingSupplierId)) &&
2869
2874
  (!draft.isCustomerSpecific || draft.customerOrganizationIds.length > 0);
2870
2875
  const onUploadPhoto = async (file) => {
2871
- if (!file || !activeId)
2876
+ if (!file)
2872
2877
  return;
2873
2878
  setUploadingPhoto(true);
2874
2879
  try {
2875
2880
  const { prepareFloridayImage } = await import("../../lib/image-resize.js");
2876
2881
  const prepared = await prepareFloridayImage(file);
2877
- const res = await floriday.uploadImage(activeId, prepared, prepared.type || "image/jpeg", prepared.name);
2878
- if (!res.url)
2879
- throw new Error("Geen URL ontvangen van Floriday");
2882
+ // Niet naar Floriday bij het kiezen; uploaden gebeurt pas bij versturen.
2883
+ const pendingUrl = registerPendingPhoto(prepared);
2880
2884
  setDraft((d) => ({
2881
2885
  ...d,
2882
- photoUrl: res.url,
2883
- photoImageId: res.imageId ?? undefined,
2886
+ photoUrl: pendingUrl,
2887
+ photoImageId: undefined,
2884
2888
  photoPreviewUrl: URL.createObjectURL(prepared),
2885
2889
  }));
2886
2890
  toast.success("Foto toegevoegd aan belading");
@@ -1 +1 @@
1
- {"version":3,"file":"EditArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/EditArtikelPage.tsx"],"names":[],"mappings":"AAaA,wBAAgB,eAAe,CAAC,EAC9B,EAAE,EACF,WAAW,GACZ,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,+BAoFA"}
1
+ {"version":3,"file":"EditArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/EditArtikelPage.tsx"],"names":[],"mappings":"AAYA,wBAAgB,eAAe,CAAC,EAC9B,EAAE,EACF,WAAW,GACZ,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,+BA4EA"}
@@ -1,10 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { Card } from "@flowselections/core";
4
4
  import { Loader2 } from "lucide-react";
5
5
  import { ArtikelWizard } from "./ArtikelWizard.js";
6
- import { ProductCustomFieldsCard } from "./ProductCustomFieldsCard.js";
7
- import { TrayStickerCard } from "./TrayStickerCard.js";
6
+ import { ArtikelPagina } from "./artikel/ArtikelPagina.js";
8
7
  import { supabase } from "../../integrations/supabase/client.js";
9
8
  export function EditArtikelPage({ id, resumeJobId, }) {
10
9
  const [resolved, setResolved] = useState(null);
@@ -64,7 +63,7 @@ export function EditArtikelPage({ id, resumeJobId, }) {
64
63
  return (_jsx("main", { className: "p-6", children: _jsxs(Card, { className: "p-6 text-sm space-y-1", children: [_jsx("div", { children: "Artikel niet gevonden." }), resolved.reason && (_jsx("div", { className: "text-muted-foreground", children: resolved.reason }))] }) }));
65
64
  }
66
65
  if (resolved.kind === "product") {
67
- return (_jsxs(_Fragment, { children: [_jsx(ArtikelWizard, { productId: resolved.id, resumeJobId: resumeJobId }), _jsxs("div", { className: "p-6 pt-0 space-y-6", children: [_jsx(TrayStickerCard, { productId: resolved.id }), _jsx(ProductCustomFieldsCard, { productId: resolved.id })] })] }));
66
+ return _jsx(ArtikelPagina, { id: resolved.id, resumeJobId: resumeJobId });
68
67
  }
69
68
  return _jsx(ArtikelWizard, { tradeItemId: resolved.id, resumeJobId: resumeJobId });
70
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProductCustomFieldsCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductCustomFieldsCard.tsx"],"names":[],"mappings":"AAyBA,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,sCA6H3E"}
1
+ {"version":3,"file":"ProductCustomFieldsCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductCustomFieldsCard.tsx"],"names":[],"mappings":"AAuBA,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,sCA+I3E"}
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
- import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
4
- import { Loader2, Save } from "lucide-react";
3
+ import { useOpslagDeelnemer } from "./artikel/opslaan-context.js";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
5
5
  import { useVoorraadCustomFields } from "../../hooks/useVoorraadCustomFields.js";
6
6
  import { fetchProductCustomValues, saveProductCustomValues, } from "../../lib/voorraad-custom-fields.js";
7
7
  /** Eigen voorraadvelden met `show_in_detail`, tonen en bewerken op de productpagina. */
8
8
  export function ProductCustomFieldsCard({ productId }) {
9
9
  const { fields, loading } = useVoorraadCustomFields();
10
10
  const [values, setValues] = useState({});
11
+ const [opened, setOpened] = useState({});
11
12
  const [loadingValues, setLoadingValues] = useState(true);
12
13
  const [saving, setSaving] = useState(false);
13
14
  useEffect(() => {
@@ -15,12 +16,16 @@ export function ProductCustomFieldsCard({ productId }) {
15
16
  setLoadingValues(true);
16
17
  fetchProductCustomValues(productId)
17
18
  .then((v) => {
18
- if (!cancelled)
19
+ if (!cancelled) {
19
20
  setValues(v);
21
+ setOpened(v);
22
+ }
20
23
  })
21
24
  .catch(() => {
22
- if (!cancelled)
25
+ if (!cancelled) {
23
26
  setValues({});
27
+ setOpened({});
28
+ }
24
29
  })
25
30
  .finally(() => {
26
31
  if (!cancelled)
@@ -31,25 +36,36 @@ export function ProductCustomFieldsCard({ productId }) {
31
36
  };
32
37
  }, [productId]);
33
38
  const detailFields = fields.filter((f) => f.show_in_detail);
34
- if (loading || loadingValues) {
35
- return null;
36
- }
37
- if (detailFields.length === 0)
38
- return null;
39
39
  const set = (key, value) => setValues((prev) => ({ ...prev, [key]: value }));
40
+ const gewijzigd = detailFields.filter((f) => JSON.stringify(values[f.field_key] ?? null) !== JSON.stringify(opened[f.field_key] ?? null));
41
+ useOpslagDeelnemer("eigen_velden", gewijzigd.length > 0, () => save());
40
42
  const save = async () => {
43
+ if (gewijzigd.length === 0)
44
+ return { ok: true };
41
45
  setSaving(true);
42
46
  try {
43
- await saveProductCustomValues(productId, Object.fromEntries(detailFields.map((f) => [f.field_key, values[f.field_key] ?? null])));
44
- toast.success("Eigen velden opgeslagen");
47
+ const res = await saveProductCustomValues(productId, Object.fromEntries(gewijzigd.map((f) => [f.field_key, opened[f.field_key] ?? null])), Object.fromEntries(gewijzigd.map((f) => [f.field_key, values[f.field_key] ?? null])));
48
+ if (!res.ok) {
49
+ const namen = res.conflicten
50
+ .map((c) => detailFields.find((f) => f.field_key === c.veld)?.field_label ?? c.veld)
51
+ .join(", ");
52
+ const fresh = await fetchProductCustomValues(productId);
53
+ setOpened(fresh);
54
+ setValues(fresh);
55
+ return { ok: false, melding: `Eigen velden niet opgeslagen: iemand anders wijzigde intussen ${namen}. De nieuwste waarden zijn geladen; vul je wijziging opnieuw in.` };
56
+ }
57
+ setOpened((prev) => ({ ...prev, ...Object.fromEntries(gewijzigd.map((f) => [f.field_key, values[f.field_key] ?? null])) }));
58
+ return { ok: true };
45
59
  }
46
60
  catch (e) {
47
- toast.error(`Opslaan mislukt: ${e.message}`);
61
+ return { ok: false, melding: `Eigen velden: opslaan mislukt (${e.message}).` };
48
62
  }
49
63
  finally {
50
64
  setSaving(false);
51
65
  }
52
66
  };
67
+ if (loading || loadingValues || detailFields.length === 0)
68
+ return null;
53
69
  return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Eigen velden" }), _jsx(CardDescription, { children: "Extra voorraadvelden zoals ingesteld bij Instellingen \u2192 Voorraadvelden." })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: detailFields.map((f) => {
54
70
  const v = values[f.field_key];
55
71
  return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: `cf-${f.field_key}`, children: f.field_label }), f.field_type === "boolean" ? (_jsx("div", { className: "pt-1", children: _jsx(Checkbox, { id: `cf-${f.field_key}`, checked: v === true, onCheckedChange: (c) => set(f.field_key, c === true) }) })) : f.field_type === "select" ? (_jsxs(Select, { value: typeof v === "string" ? v : "", onValueChange: (val) => set(f.field_key, val), children: [_jsx(SelectTrigger, { id: `cf-${f.field_key}`, children: _jsx(SelectValue, { placeholder: "Kies\u2026" }) }), _jsx(SelectContent, { children: f.options.map((o) => (_jsx(SelectItem, { value: o, children: o }, o))) })] })) : (_jsx(Input, { id: `cf-${f.field_key}`, type: f.field_type === "number"
@@ -61,5 +77,5 @@ export function ProductCustomFieldsCard({ productId }) {
61
77
  ? null
62
78
  : Number(e.target.value)
63
79
  : e.target.value) }))] }, f.id));
64
- }) }), _jsxs(Button, { onClick: save, disabled: saving, children: [saving ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Save, { className: "mr-2 h-4 w-4" })), "Eigen velden opslaan"] })] })] }));
80
+ }) }), gewijzigd.length > 0 && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Niet opgeslagen wijzigingen; bewaar met \"Opslaan\" bovenaan." }))] })] }));
65
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProductSalePriceField.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductSalePriceField.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,sCAkGA;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,sCAqCtE"}
1
+ {"version":3,"file":"ProductSalePriceField.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductSalePriceField.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,sCAoGA;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,sCAqCtE"}
@@ -58,18 +58,19 @@ export function ProductSalePriceField({ productId, onSaved, }) {
58
58
  setSaving(true);
59
59
  setError(null);
60
60
  try {
61
- const { data, error: err } = await supabase
62
- .from("products")
63
- .update({ sale_price: parsed })
64
- .eq("id", productId)
65
- .select("id");
61
+ // Zelfde opslagroute als "Opslaan" op de artikelpagina: conflictcontrole per veld.
62
+ const { data, error: err } = await supabase.rpc("voorraad_artikel_opslaan", {
63
+ p_id: productId, p_oud: { sale_price: saved }, p_nieuw: { sale_price: parsed },
64
+ });
66
65
  if (err)
67
66
  throw new Error(err.message);
68
- if (!data || data.length === 0)
69
- throw new Error("geen rechten om dit artikel te wijzigen");
67
+ if (!data?.ok) {
68
+ const hun = data?.conflicten?.[0]?.hun;
69
+ throw new Error(`iemand anders heeft de prijs intussen gewijzigd${hun != null ? ` naar € ${hun}` : ""}. Laad het artikel opnieuw`);
70
+ }
70
71
  setSaved(parsed);
71
72
  onSaved?.(parsed);
72
- toast.success("Prijs opgeslagen. Het artikel gaat opnieuw naar Floriday.");
73
+ toast.success("Prijs opgeslagen (alleen lokaal).");
73
74
  }
74
75
  catch (e) {
75
76
  const msg = `Prijs niet opgeslagen: ${e?.message ?? e}`;
@@ -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;AA6FD,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,UAAU,EACV,QAAQ,EACR,kBAAkB,GACnB,EAAE,KAAK,+BA+aP"}
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;AAwB5E,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,+BAqcP"}
@@ -94,6 +94,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
94
94
  const [loading, setLoading] = useState(true);
95
95
  const [saving, setSaving] = useState(false);
96
96
  const [rows, setRows] = useState([]);
97
+ const [teVerwijderen, setTeVerwijderen] = useState([]);
97
98
  const [names, setNames] = useState(new Map());
98
99
  const [activeTab, setActiveTab] = useState("__base__");
99
100
  const hasParent = Boolean(productId || floridayId);
@@ -131,8 +132,10 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
131
132
  component_trade_item_floriday_id: r.component_trade_item_floriday_id ?? null,
132
133
  quantity: Number(r.quantity ?? 1),
133
134
  sort_order: Number(r.sort_order ?? 0),
135
+ _orig: { quantity: Number(r.quantity ?? 1), sort_order: Number(r.sort_order ?? 0) },
134
136
  }));
135
137
  setRows(list);
138
+ setTeVerwijderen([]);
136
139
  await resolveNames(list);
137
140
  setLoading(false);
138
141
  }, [productId, floridayId, hasParent]);
@@ -230,13 +233,9 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
230
233
  const row = rows.find((r) => r.id === id);
231
234
  if (!row)
232
235
  return;
233
- if (!row._new) {
234
- const { error } = await supabase.from("product_bom_items").delete().eq("id", id);
235
- if (error) {
236
- toast.error("Verwijderen mislukt");
237
- return;
238
- }
239
- }
236
+ // Verwijderen gebeurt pas bij "Opslaan" (bewuste actie met conflictcontrole).
237
+ if (!row._new)
238
+ setTeVerwijderen((l) => [...l, row]);
240
239
  setRows((prev) => prev.filter((r) => r.id !== id));
241
240
  };
242
241
  const saveAll = async () => {
@@ -245,7 +244,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
245
244
  return;
246
245
  }
247
246
  const dirty = rows.filter((r) => r._dirty);
248
- if (dirty.length === 0)
247
+ if (dirty.length === 0 && teVerwijderen.length === 0)
249
248
  return;
250
249
  setSaving(true);
251
250
  // Verhoudingscontrole: alle lagen meetellen, nooit blokkeren.
@@ -264,6 +263,32 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
264
263
  // Controle mislukt: opslaan gaat gewoon door.
265
264
  }
266
265
  try {
266
+ // Conflictcontrole: bestaande regels die je wijzigt of verwijdert moeten nog
267
+ // dezelfde stand hebben als bij openen.
268
+ const gecontroleerd = [...dirty.filter((r) => !r._new), ...teVerwijderen].filter((r) => r._orig);
269
+ if (gecontroleerd.length) {
270
+ const { data: nu, error: nuErr } = await supabase
271
+ .from("product_bom_items")
272
+ .select("id,quantity,sort_order")
273
+ .in("id", gecontroleerd.map((r) => r.id));
274
+ if (nuErr)
275
+ throw nuErr;
276
+ const huidig = new Map((nu ?? []).map((r) => [r.id, r]));
277
+ const afwijkend = gecontroleerd.filter((r) => {
278
+ const h = huidig.get(r.id);
279
+ return !h || Number(h.quantity) !== r._orig.quantity || Number(h.sort_order) !== r._orig.sort_order;
280
+ });
281
+ if (afwijkend.length) {
282
+ toast.error("Iemand anders heeft deze samenstelling intussen gewijzigd. Er is niets opgeslagen; laad de samenstelling opnieuw.");
283
+ return;
284
+ }
285
+ }
286
+ for (const r of teVerwijderen) {
287
+ const { error } = await supabase.from("product_bom_items").delete().eq("id", r.id);
288
+ if (error)
289
+ throw error;
290
+ }
291
+ setTeVerwijderen([]);
267
292
  const inserts = dirty
268
293
  .filter((r) => r._new)
269
294
  .map((r) => ({
@@ -309,7 +334,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
309
334
  if (error)
310
335
  throw error;
311
336
  }
312
- setRows((prev) => prev.map((r) => ({ ...r, _dirty: false })));
337
+ setRows((prev) => prev.map((r) => ({ ...r, _dirty: false, _orig: { quantity: r.quantity, sort_order: r.sort_order } })));
313
338
  toast.success("Samenstelling opgeslagen");
314
339
  }
315
340
  catch (e) {
@@ -319,7 +344,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
319
344
  setSaving(false);
320
345
  }
321
346
  };
322
- const hasUnsaved = rows.some((r) => r._dirty);
347
+ const hasUnsaved = rows.some((r) => r._dirty) || teVerwijderen.length > 0;
323
348
  if (!hasParent) {
324
349
  return (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground", children: "Sla het artikel eerst op om een samenstelling te configureren." }));
325
350
  }
@@ -1,4 +1,8 @@
1
- /** Traysticker-afbeelding per product — los van de Floriday-productfoto (`products.image_url`). */
1
+ /**
2
+ * Traysticker-afbeelding per artikel — los van de Floriday-productfoto.
3
+ * Kiezen of verwijderen wordt pas bewaard met de knop "Opslaan" van de pagina,
4
+ * met controle tegen de stand bij openen.
5
+ */
2
6
  export declare function TrayStickerCard({ productId }: {
3
7
  productId: string;
4
8
  }): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"TrayStickerCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/TrayStickerCard.tsx"],"names":[],"mappings":"AAyBA,mGAAmG;AACnG,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,+BA+HnE"}
1
+ {"version":3,"file":"TrayStickerCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/TrayStickerCard.tsx"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,+BAoInE"}