@flowselections/floriday-voorraad 1.0.139 → 1.0.143

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 (34) hide show
  1. package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/NieuwArtikelPage.js +22 -3
  3. package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts.map +1 -1
  4. package/dist-lib/components/voorraad/StockMutationHistoryCard.js +3 -2
  5. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  6. package/dist-lib/components/voorraad/VoorraadPage.js +2 -0
  7. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
  8. package/dist-lib/components/voorraad/VoorraadStockCard.js +28 -60
  9. package/dist-lib/hooks/useEnsureVoorraadDefaults.d.ts +7 -0
  10. package/dist-lib/hooks/useEnsureVoorraadDefaults.d.ts.map +1 -0
  11. package/dist-lib/hooks/useEnsureVoorraadDefaults.js +25 -0
  12. package/dist-lib/hooks/useTradeItemStock.d.ts +4 -1
  13. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
  14. package/dist-lib/hooks/useTradeItemStock.js +31 -44
  15. package/dist-lib/integrations/supabase/types.d.ts +421 -0
  16. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  17. package/dist-lib/lib/__tests__/release-140.test.d.ts +2 -0
  18. package/dist-lib/lib/__tests__/release-140.test.d.ts.map +1 -0
  19. package/dist-lib/lib/__tests__/release-140.test.js +42 -0
  20. package/dist-lib/lib/db-error-message.d.ts +7 -0
  21. package/dist-lib/lib/db-error-message.d.ts.map +1 -0
  22. package/dist-lib/lib/db-error-message.js +36 -0
  23. package/dist-lib/lib/engine-carrier.d.ts +15 -0
  24. package/dist-lib/lib/engine-carrier.d.ts.map +1 -0
  25. package/dist-lib/lib/engine-carrier.js +27 -0
  26. package/dist-lib/lib/simple-packing.d.ts +2 -4
  27. package/dist-lib/lib/simple-packing.d.ts.map +1 -1
  28. package/dist-lib/lib/simple-packing.js +5 -12
  29. package/dist-lib/lib/stock-save.d.ts +11 -0
  30. package/dist-lib/lib/stock-save.d.ts.map +1 -1
  31. package/dist-lib/lib/stock-save.js +17 -0
  32. package/package.json +4 -2
  33. package/sql/AGENTS.md +4 -0
  34. package/sql/floriday-voorraad-schema.sql +441 -82
@@ -1 +1 @@
1
- {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,gBAAgB,gCA4X/B"}
1
+ {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,gBAAgB,gCA4Y/B"}
@@ -19,6 +19,7 @@ import { useLoadCarriers } from "../../hooks/useLoadCarriers.js";
19
19
  import { saveOwnPackings } from "../../lib/product-packings.js";
20
20
  import { assertArticleCode } from "../../lib/article-code.js";
21
21
  import { EMPTY_SIMPLE_PACKING, engineOordeelRegel, missingPackingFields, simplePackingToRow, } from "../../lib/simple-packing.js";
22
+ import { resolveEngineCarrier } from "../../lib/engine-carrier.js";
22
23
  const STEPS = ["Naam en code", "Voorraad", "Prijs", "Belading"];
23
24
  export function NieuwArtikelPage() {
24
25
  const carriers = useLoadCarriers();
@@ -41,6 +42,22 @@ export function NieuwArtikelPage() {
41
42
  const [fusts, setFusts] = useState([]);
42
43
  const [oordeel, setOordeel] = useState(null);
43
44
  const missingPacking = missingPackingFields(packing);
45
+ // Vertaling naar de Engine-code: alleen via de Logistiek Engine, geen eigen lijst.
46
+ const [engineCarrier, setEngineCarrier] = useState({ code: null, melding: null });
47
+ useEffect(() => {
48
+ let off = false;
49
+ setEngineCarrier({ code: null, melding: null });
50
+ if (!packing.loadCarrier)
51
+ return;
52
+ void resolveEngineCarrier(supabase, packing.loadCarrier).then((r) => {
53
+ if (!off)
54
+ setEngineCarrier(r);
55
+ });
56
+ return () => {
57
+ off = true;
58
+ };
59
+ }, [packing.loadCarrier]);
60
+ const packingReady = missingPacking.length === 0 && !!engineCarrier.code;
44
61
  const setPack = (k, v) => setPacking((p) => ({ ...p, [k]: v }));
45
62
  useEffect(() => {
46
63
  let off = false;
@@ -84,7 +101,7 @@ export function NieuwArtikelPage() {
84
101
  : step === 1
85
102
  ? !canSaveStock || (Number(stock) >= 0 && stock !== "")
86
103
  : step === 3
87
- ? missingPacking.length === 0
104
+ ? packingReady
88
105
  : true;
89
106
  const save = async () => {
90
107
  if (savingRef.current)
@@ -98,6 +115,8 @@ export function NieuwArtikelPage() {
98
115
  throw new Error("Vul een geldige prijs in (groter dan 0)");
99
116
  assertArticleCode(code, name, "manual");
100
117
  const packingRow = simplePackingToRow(packing);
118
+ if (!engineCarrier.code)
119
+ throw new Error(engineCarrier.melding ?? "De ladingdrager wordt nog gecontroleerd door de Logistiek Engine.");
101
120
  let id = insertedIdRef.current;
102
121
  if (!id) {
103
122
  const { data, error } = await supabase
@@ -139,7 +158,7 @@ export function NieuwArtikelPage() {
139
158
  }
140
159
  // Controle door de Engine (alleen lezen); uitkomst tonen, niet blokkeren.
141
160
  const { data: oo, error: ooErr } = await supabase.rpc("logistics_product_oordeel_belading", {
142
- p_regels: [engineOordeelRegel(id, packing)],
161
+ p_regels: [engineOordeelRegel(id, packing, engineCarrier.code)],
143
162
  });
144
163
  const first = Array.isArray(oo) ? oo[0] : null;
145
164
  setOordeel(ooErr
@@ -167,5 +186,5 @@ export function NieuwArtikelPage() {
167
186
  ? "rounded-md border p-3 text-sm"
168
187
  : "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive", children: [_jsxs("p", { className: "font-medium", children: ["Controle karindeling: ", oordeel.tekst] }), oordeel.regels.length > 0 && (_jsx("ul", { className: "list-disc pl-5", children: oordeel.regels.map((r) => (_jsx("li", { children: r }, r))) }))] })), hasFloriday ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Wil je het artikel ook op Floriday zetten? Daarvoor is nog nodig:" }), _jsxs("ul", { className: "list-disc pl-5 text-sm", children: [!hasPrice && _jsx("li", { children: "Prijs per stuk (excl. btw)" }), missing.map((m) => (_jsx("li", { children: m }, m)))] })] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "De voorraad is opgeslagen als eigen voorraad. Die gaat niet naar Floriday." })), _jsxs("div", { className: "flex flex-wrap gap-2", children: [hasFloriday && (_jsxs(Button, { onClick: () => navigate({ to: "/voorraad/$id", params: { id: createdId } }), children: [_jsx(Send, { className: "mr-2 h-4 w-4" }), "Op Floriday zetten"] })), _jsx(Button, { variant: "outline", asChild: true, children: _jsx(Link, { to: "/voorraad", children: "Terug naar voorraad" }) })] })] }) }));
169
188
  }
170
- return (_jsxs("main", { className: "p-6 space-y-4 max-w-2xl", children: [_jsxs(Link, { to: "/voorraad", className: "inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Terug naar voorraad"] }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Artikel toevoegen" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Stap ", step + 1, " van ", STEPS.length, ": ", STEPS[step]] })] }), _jsx(FloridayConnectionNotice, { status: conn.status, environment: conn.selectedEnvironment, error: conn.error }), _jsxs(Card, { className: "p-5 space-y-4", children: [step === 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-name", children: "Naam" }), _jsx(Input, { id: "na-name", value: name, onChange: (e) => setName(e.target.value), autoFocus: true })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-code", children: "Artikelcode (optioneel)" }), _jsx(Input, { id: "na-code", value: code, onChange: (e) => setCode(e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Leeg laten: er wordt automatisch een code gemaakt." })] })] })), step === 1 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-stock", children: "Voorraad (stuks)" }), _jsx(Input, { id: "na-stock", type: "number", min: 0, value: canSaveStock ? stock : "", onChange: (e) => setStock(e.target.value), disabled: !canSaveStock, "aria-describedby": "na-stock-help", autoFocus: true }), !canSaveStock && (_jsx("p", { id: "na-stock-help", className: "text-xs text-muted-foreground", children: "Koppeling wordt gecontroleerd\u2026" })), canSaveStock && !hasFloriday && (_jsx("p", { id: "na-stock-help", className: "text-xs text-muted-foreground", children: LOCAL_STOCK_TEXT }))] })), step === 2 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-price", children: "Prijs per stuk (EUR, excl. btw)" }), _jsx(Input, { id: "na-price", inputMode: "decimal", value: price, onChange: (e) => setPrice(e.target.value), placeholder: "Bijv. 1,10", autoFocus: true }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Zonder prijs komt het artikel niet in Floriday (\"Prijs ontbreekt\")." })] })), step === 3 && (_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Zonder volledige belading kan de karindeling dit artikel niet plannen. Alle vijf velden zijn verplicht." }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-fust", children: "Fust (VBN-code)" }), _jsxs("select", { id: "na-fust", className: "w-full rounded-md border bg-background px-3 py-2 text-sm", value: packing.vbnPackageCode, onChange: (e) => setPack("vbnPackageCode", e.target.value), children: [_jsx("option", { value: "", children: "Kies een fust\u2026" }), fusts.map((f) => (_jsxs("option", { value: String(f.vbn), children: [f.vbn, " \u2013 ", f.name] }, f.vbn)))] }), _jsx(Input, { "aria-label": "Andere VBN-fustcode", placeholder: "Of typ een VBN-fustcode, bijv. 412", inputMode: "numeric", value: packing.vbnPackageCode, onChange: (e) => setPack("vbnPackageCode", e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-ppc", children: "Planten per fust" }), _jsx(Input, { id: "na-ppc", inputMode: "numeric", value: packing.piecesPerPackage, onChange: (e) => setPack("piecesPerPackage", e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-cpl", children: "Fusten per laag" }), _jsx(Input, { id: "na-cpl", inputMode: "numeric", value: packing.packagesPerLayer, onChange: (e) => setPack("packagesPerLayer", e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-lpc", children: "Lagen per kar" }), _jsx(Input, { id: "na-lpc", inputMode: "numeric", value: packing.layersPerLoadCarrier, onChange: (e) => setPack("layersPerLoadCarrier", e.target.value) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-lc", children: "Ladingdrager" }), _jsxs("select", { id: "na-lc", className: "w-full rounded-md border bg-background px-3 py-2 text-sm", value: packing.loadCarrier, onChange: (e) => setPack("loadCarrier", e.target.value), children: [_jsx("option", { value: "", children: "Kies een ladingdrager\u2026" }), carriers.items.map((c) => (_jsx("option", { value: c.code, children: c.naam }, c.code)))] }), carriers.error && _jsx("p", { className: "text-xs text-destructive", children: carriers.error })] }), missingPacking.length > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Nog invullen: ", missingPacking.join(", "), "."] }))] })), saveError && (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive", children: saveError })), _jsxs("div", { className: "flex flex-wrap justify-between gap-2", children: [_jsx(Button, { variant: "outline", disabled: step === 0 || saving, onClick: () => setStep((s) => s - 1), children: "Vorige" }), step < STEPS.length - 1 ? (_jsx(Button, { disabled: !canNext, onClick: () => setStep((s) => s + 1), children: "Volgende" })) : (_jsxs(Button, { disabled: saving || !name.trim() || missingPacking.length > 0, onClick: () => void save(), children: [saving ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(Check, { className: "mr-2 h-4 w-4" }), saving ? "Bezig met opslaan…" : "Artikel opslaan"] }))] })] }), hasFloriday && (_jsx("button", { type: "button", className: "text-sm text-muted-foreground underline hover:text-foreground", onClick: () => setFull(true), children: "Meteen alle Floriday-gegevens invullen" }))] }));
189
+ return (_jsxs("main", { className: "p-6 space-y-4 max-w-2xl", children: [_jsxs(Link, { to: "/voorraad", className: "inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Terug naar voorraad"] }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Artikel toevoegen" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Stap ", step + 1, " van ", STEPS.length, ": ", STEPS[step]] })] }), _jsx(FloridayConnectionNotice, { status: conn.status, environment: conn.selectedEnvironment, error: conn.error }), _jsxs(Card, { className: "p-5 space-y-4", children: [step === 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-name", children: "Naam" }), _jsx(Input, { id: "na-name", value: name, onChange: (e) => setName(e.target.value), autoFocus: true })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-code", children: "Artikelcode (optioneel)" }), _jsx(Input, { id: "na-code", value: code, onChange: (e) => setCode(e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Leeg laten: er wordt automatisch een code gemaakt." })] })] })), step === 1 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-stock", children: "Voorraad (stuks)" }), _jsx(Input, { id: "na-stock", type: "number", min: 0, value: canSaveStock ? stock : "", onChange: (e) => setStock(e.target.value), disabled: !canSaveStock, "aria-describedby": "na-stock-help", autoFocus: true }), !canSaveStock && (_jsx("p", { id: "na-stock-help", className: "text-xs text-muted-foreground", children: "Koppeling wordt gecontroleerd\u2026" })), canSaveStock && !hasFloriday && (_jsx("p", { id: "na-stock-help", className: "text-xs text-muted-foreground", children: LOCAL_STOCK_TEXT }))] })), step === 2 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-price", children: "Prijs per stuk (EUR, excl. btw)" }), _jsx(Input, { id: "na-price", inputMode: "decimal", value: price, onChange: (e) => setPrice(e.target.value), placeholder: "Bijv. 1,10", autoFocus: true }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Zonder prijs komt het artikel niet in Floriday (\"Prijs ontbreekt\")." })] })), step === 3 && (_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Zonder volledige belading kan de karindeling dit artikel niet plannen. Alle vijf velden zijn verplicht." }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-fust", children: "Fust (VBN-code)" }), _jsxs("select", { id: "na-fust", className: "w-full rounded-md border bg-background px-3 py-2 text-sm", value: packing.vbnPackageCode, onChange: (e) => setPack("vbnPackageCode", e.target.value), children: [_jsx("option", { value: "", children: "Kies een fust\u2026" }), fusts.map((f) => (_jsxs("option", { value: String(f.vbn), children: [f.vbn, " \u2013 ", f.name] }, f.vbn)))] }), _jsx(Input, { "aria-label": "Andere VBN-fustcode", placeholder: "Of typ een VBN-fustcode, bijv. 412", inputMode: "numeric", value: packing.vbnPackageCode, onChange: (e) => setPack("vbnPackageCode", e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-ppc", children: "Planten per fust" }), _jsx(Input, { id: "na-ppc", inputMode: "numeric", value: packing.piecesPerPackage, onChange: (e) => setPack("piecesPerPackage", e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-cpl", children: "Fusten per laag" }), _jsx(Input, { id: "na-cpl", inputMode: "numeric", value: packing.packagesPerLayer, onChange: (e) => setPack("packagesPerLayer", e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-lpc", children: "Lagen per kar" }), _jsx(Input, { id: "na-lpc", inputMode: "numeric", value: packing.layersPerLoadCarrier, onChange: (e) => setPack("layersPerLoadCarrier", e.target.value) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-lc", children: "Ladingdrager" }), _jsxs("select", { id: "na-lc", className: "w-full rounded-md border bg-background px-3 py-2 text-sm", value: packing.loadCarrier, onChange: (e) => setPack("loadCarrier", e.target.value), children: [_jsx("option", { value: "", children: "Kies een ladingdrager\u2026" }), carriers.items.map((c) => (_jsx("option", { value: c.code, children: c.naam }, c.code)))] }), carriers.error && _jsx("p", { className: "text-xs text-destructive", children: carriers.error }), engineCarrier.melding && _jsx("p", { className: "text-xs text-destructive", children: engineCarrier.melding })] }), missingPacking.length > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Nog invullen: ", missingPacking.join(", "), "."] }))] })), saveError && (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive", children: saveError })), _jsxs("div", { className: "flex flex-wrap justify-between gap-2", children: [_jsx(Button, { variant: "outline", disabled: step === 0 || saving, onClick: () => setStep((s) => s - 1), children: "Vorige" }), step < STEPS.length - 1 ? (_jsx(Button, { disabled: !canNext, onClick: () => setStep((s) => s + 1), children: "Volgende" })) : (_jsxs(Button, { disabled: saving || !name.trim() || !packingReady, onClick: () => void save(), children: [saving ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(Check, { className: "mr-2 h-4 w-4" }), saving ? "Bezig met opslaan…" : "Artikel opslaan"] }))] })] }), hasFloriday && (_jsx("button", { type: "button", className: "text-sm text-muted-foreground underline hover:text-foreground", onClick: () => setFull(true), children: "Meteen alle Floriday-gegevens invullen" }))] }));
171
190
  }
@@ -1 +1 @@
1
- {"version":3,"file":"StockMutationHistoryCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockMutationHistoryCard.tsx"],"names":[],"mappings":"AAWA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAiDD,wBAAgB,wBAAwB,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,KAAK,+BA4O1E"}
1
+ {"version":3,"file":"StockMutationHistoryCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockMutationHistoryCard.tsx"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAiDD,wBAAgB,wBAAwB,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,KAAK,+BA4O1E"}
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  * de groepssleutel wanneer het artikel via een groep wordt afgeboekt).
6
6
  */
7
7
  import { useEffect, useState } from "react";
8
+ import { dbFoutTekst } from "../../lib/db-error-message.js";
8
9
  import { Card, CardContent, Badge, Button } from "@flowselections/core";
9
10
  import { ChevronDown, ChevronUp, Loader2, History } from "lucide-react";
10
11
  import { supabase } from "../../integrations/supabase/client.js";
@@ -122,7 +123,7 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
122
123
  }
123
124
  catch (e) {
124
125
  if (!cancelled)
125
- setError(e.message);
126
+ setError(dbFoutTekst(e, "Geschiedenis laden"));
126
127
  }
127
128
  };
128
129
  load();
@@ -135,7 +136,7 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
135
136
  supabase.removeChannel(channel);
136
137
  };
137
138
  }, [tradeItemKey, productId]);
138
- 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(History, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Afboekgeschiedenis" }), _jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: rows ? `${rows.length} mutatie${rows.length === 1 ? "" : "s"}` : "" })] }), _jsxs(CardContent, { className: "pt-0", children: [error && (_jsxs("div", { className: "text-sm text-destructive", children: ["Kon voorraadmutaties niet laden: ", error] })), !rows && !error && (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Mutaties laden\u2026"] })), rows && rows.length === 0 && !error && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen voorraadmutaties voor dit artikel." })), rows && rows.length > 0 && (_jsxs("div", { className: "overflow-x-auto rounded-md border", children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left", children: "Wanneer" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Soort" }), _jsx("th", { className: "px-3 py-2 text-right", children: "Aantal" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Van \u2192 naar" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Order" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Door" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Reden" })] }) }), _jsx("tbody", { children: (expanded ? rows : rows.slice(0, 5)).map((r) => {
139
+ 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(History, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Afboekgeschiedenis" }), _jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: rows ? `${rows.length} mutatie${rows.length === 1 ? "" : "s"}` : "" })] }), _jsxs(CardContent, { className: "pt-0", children: [error && (_jsx("div", { className: "text-sm text-destructive", children: error })), !rows && !error && (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Mutaties laden\u2026"] })), rows && rows.length === 0 && !error && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen voorraadmutaties voor dit artikel." })), rows && rows.length > 0 && (_jsxs("div", { className: "overflow-x-auto rounded-md border", children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left", children: "Wanneer" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Soort" }), _jsx("th", { className: "px-3 py-2 text-right", children: "Aantal" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Van \u2192 naar" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Order" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Door" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Reden" })] }) }), _jsx("tbody", { children: (expanded ? rows : rows.slice(0, 5)).map((r) => {
139
140
  const meta = TYPE_LABEL[r.mutation_type] ?? {
140
141
  label: r.mutation_type || "Onbekend",
141
142
  tone: "bg-slate-100 text-slate-700 border-slate-200",
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAmF3D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,mBAAmB,GAAG,aAAa,CAAC,CAAC;;;;;;;EAgBzG;AAED,wBAAgB,YAAY,gCAqW3B"}
1
+ {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAmF3D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,mBAAmB,GAAG,aAAa,CAAC,CAAC;;;;;;;EAgBzG;AAED,wBAAgB,YAAY,gCAsW3B"}
@@ -7,6 +7,7 @@ import { Link } from "@tanstack/react-router";
7
7
  import { Button, Card, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
8
8
  import { AlertTriangle, CheckCircle2, FileText, PackageX, Plus, Search, } from "lucide-react";
9
9
  import { useFloridayConnection } from "../../hooks/useFloridayConnection.js";
10
+ import { useEnsureVoorraadDefaults } from "../../hooks/useEnsureVoorraadDefaults.js";
10
11
  import { FloridayConnectionNotice } from "./FloridayConnectionNotice.js";
11
12
  import { useVoorraadData } from "../../hooks/useVoorraadData.js";
12
13
  import { StockSyncStatus } from "./StockSyncStatus.js";
@@ -69,6 +70,7 @@ export function countStock(items) {
69
70
  return { total: items.length, inStock, outOfStock, belowMin, fullyReserved, freeAvailable };
70
71
  }
71
72
  export function VoorraadPage() {
73
+ useEnsureVoorraadDefaults();
72
74
  const conn = useFloridayConnection();
73
75
  const { activeId, error: connError } = conn;
74
76
  const { items, loading, error, reload } = useVoorraadData(activeId);
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AA4BA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,KAAK,sCAiTzF"}
1
+ {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AA4BA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,KAAK,sCA+RzF"}
@@ -4,9 +4,9 @@ import { Button, Card, CardContent, Input, Label, Textarea, toast, Dialog, Dialo
4
4
  import { Loader2, Package, Check } from "lucide-react";
5
5
  import { useTradeItemStock, stockKeysFor } from "../../hooks/useTradeItemStock.js";
6
6
  import { supabase } from "../../integrations/supabase/client.js";
7
- import { FloridaySentCard } from "./FloridaySentCard.js";
8
7
  import { LOCAL_STOCK_TEXT } from "../../lib/stock-scope.js";
9
- import { AFBOEK_REDENEN, afboekFout, floridayNaOpslaanTekst, valuesAfterSave } from "../../lib/stock-save.js";
8
+ import { AFBOEK_REDENEN, afboekFout, opslaanMelding, OPSLAAN_MISLUKT_TEKST, valuesAfterSave } from "../../lib/stock-save.js";
9
+ import { dbFoutTekst } from "../../lib/db-error-message.js";
10
10
  export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productId }) {
11
11
  const keys = stockKeysFor({
12
12
  tradeItemId,
@@ -18,33 +18,14 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
18
18
  const [values, setValues] = useState(row);
19
19
  const [saving, setSaving] = useState(false);
20
20
  const [touched, setTouched] = useState(false);
21
- const [hasBom, setHasBom] = useState(false);
22
21
  const [afboekOpen, setAfboekOpen] = useState(false);
23
22
  const [afboekAantal, setAfboekAantal] = useState("");
24
23
  const [afboekReden, setAfboekReden] = useState(null);
25
24
  const [afboekNotitie, setAfboekNotitie] = useState("");
26
25
  const [afboekBezig, setAfboekBezig] = useState(false);
27
- // Heeft dit artikel onderdelen (samenstelling)? Bepaalt alleen het label
28
- // in de Floriday-kaart. Produceren bestaat niet meer in deze module.
29
- useEffect(() => {
30
- let cancelled = false;
31
- (async () => {
32
- if (!productId && !tradeItemId) {
33
- setHasBom(false);
34
- return;
35
- }
36
- let query = supabase.from("product_bom_items").select("id").limit(1);
37
- query = productId
38
- ? query.eq("parent_product_id", productId)
39
- : query.eq("parent_trade_item_floriday_id", tradeItemId);
40
- const { data } = await query;
41
- if (!cancelled)
42
- setHasBom((data ?? []).length > 0);
43
- })();
44
- return () => {
45
- cancelled = true;
46
- };
47
- }, [productId, tradeItemId]);
26
+ const [wijzigingNotitie, setWijzigingNotitie] = useState("");
27
+ const [artikelNotitieOpen, setArtikelNotitieOpen] = useState(false);
28
+ const [artikelNotitieGewijzigd, setArtikelNotitieGewijzigd] = useState(false);
48
29
  // Is dit artikel de kop van een productgroep? Dan geen voorraad invoeren.
49
30
  const [isGroupHead, setIsGroupHead] = useState(false);
50
31
  useEffect(() => {
@@ -76,52 +57,36 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
76
57
  return (_jsx(Card, { className: "p-4 text-sm text-muted-foreground", children: "Voorraad kan pas worden ingesteld nadat het artikel voor het eerst is opgeslagen." }));
77
58
  }
78
59
  if (stockError) {
79
- return (_jsxs(Card, { className: "p-4 text-sm text-destructive", children: ["Voorraad laden lukt niet. Controleer of je bent ingelogd en of dit artikel aan een voorraadverbinding is gekoppeld.", _jsxs("div", { className: "mt-1 text-xs text-muted-foreground", children: ["Details: ", String(stockError)] })] }));
60
+ return (_jsx(Card, { className: "p-4 text-sm text-destructive", children: "Voorraad laden lukt niet. Controleer of je bent ingelogd en of dit artikel aan een voorraadverbinding is gekoppeld." }));
80
61
  }
81
62
  const available = Math.max(0, values.physical_stock - values.reserved_stock);
82
- // Foutmeldingen uit de database zijn soms JSON of technisch; hier maken we
83
- // er gewone taal van zodat de gebruiker weet wat er moet gebeuren.
84
- const humanError = (e) => {
85
- const raw = e?.message ?? String(e);
86
- try {
87
- const parsed = JSON.parse(raw);
88
- const msg = parsed?.message ?? parsed?.error ?? raw;
89
- if (typeof msg === "string" && msg.toLowerCase().includes("onvoldoende voorraad")) {
90
- return "Onvoldoende voorraad van een onderdeel. Pas de voorraad van het onderdeel eerst aan.";
91
- }
92
- return typeof msg === "string" ? msg : raw;
93
- }
94
- catch {
95
- return raw;
96
- }
97
- };
98
63
  // Opslaan via set_product_stock werkt ook zonder koppeling (lokale context, 1.0.107).
99
64
  const canWrite = !!productId || !!connectionId;
100
65
  const handleSave = async () => {
101
66
  if (!canWrite)
102
67
  return;
103
68
  setSaving(true);
69
+ let result = null;
70
+ let fout = null;
104
71
  try {
105
- await save(values);
106
- setValues(valuesAfterSave(true, values, row));
107
- setTouched(false);
108
- let poortOpen = null;
109
- if (tradeItemId) {
110
- const { data } = await supabase.rpc("voorraad_floriday_klaarzetten_toegestaan");
111
- poortOpen = data === true;
112
- }
113
- const extra = floridayNaOpslaanTekst(!!tradeItemId, poortOpen);
114
- toast.success(extra ? `Voorraad opgeslagen. ${extra}` : "Voorraad opgeslagen");
72
+ result = await save(values, { wijzigingNotitie, artikelNotitieWijzigen: artikelNotitieGewijzigd });
115
73
  }
116
74
  catch (e) {
117
- // Niets opgeslagen: laatst opgeslagen waarden terug in beeld.
118
- setValues(valuesAfterSave(false, values, row));
119
- setTouched(false);
120
- toast.error(`Opslaan mislukt, niets gewijzigd: ${humanError(e)}`);
75
+ fout = dbFoutTekst(e, "Opslaan");
121
76
  }
122
- finally {
123
- setSaving(false);
77
+ const melding = opslaanMelding(result, !!tradeItemId);
78
+ setValues(valuesAfterSave(melding.ok, values, row));
79
+ setTouched(false);
80
+ if (melding.ok) {
81
+ setWijzigingNotitie("");
82
+ setArtikelNotitieGewijzigd(false);
83
+ toast.success(melding.tekst);
84
+ }
85
+ else {
86
+ // Niets opgeslagen (ook bij teruggedraaide opslag): oude waarden terug.
87
+ toast.error(fout ? `${OPSLAAN_MISLUKT_TEKST}. ${fout}` : OPSLAAN_MISLUKT_TEKST);
124
88
  }
89
+ setSaving(false);
125
90
  };
126
91
  const handleAfboeken = async () => {
127
92
  const aantal = Number(afboekAantal);
@@ -145,7 +110,7 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
145
110
  p_notitie: afboekNotitie.trim() || null,
146
111
  });
147
112
  if (error)
148
- throw new Error(error.message);
113
+ throw error;
149
114
  toast.success(`${aantal} afgeboekt`);
150
115
  setAfboekOpen(false);
151
116
  setAfboekAantal("");
@@ -154,7 +119,7 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
154
119
  await refetch();
155
120
  }
156
121
  catch (e) {
157
- toast.error(`Afboeken mislukt, niets gewijzigd: ${humanError(e)}`);
122
+ toast.error(`Afboeken mislukt, niets gewijzigd. ${dbFoutTekst(e, "Afboeken")}`);
158
123
  }
159
124
  finally {
160
125
  setAfboekBezig(false);
@@ -164,5 +129,8 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
164
129
  setTouched(true);
165
130
  setValues((v) => ({ ...v, [id]: Number(e.target.value) || 0 }));
166
131
  } })] }));
167
- return (_jsxs("div", { className: "space-y-4", children: [_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(Package, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Voorraad" }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["Beschikbaar: ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: available })] })] }), _jsx(CardContent, { className: "pt-0 space-y-4", 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" }), " Voorraad laden\u2026"] })) : (_jsxs(_Fragment, { children: [Number(values.reserved_stock) > Number(values.physical_stock) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: ["Let op: er is meer gereserveerd (", Number(values.reserved_stock), ") dan er ligt (", Number(values.physical_stock), "). Controleer de open orders van dit artikel."] })), _jsxs("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-4", children: [numField("physical_stock", "Liggend"), numField("minimum_stock", "Minimum"), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "reserved_stock", children: "Gereserveerd" }), _jsx(Input, { id: "reserved_stock", type: "number", readOnly: true, disabled: true, value: String(values.reserved_stock ?? 0) }), _jsx("p", { className: "text-[11px] text-muted-foreground", children: "Berekend uit open orders" })] }), numField("incoming_stock", "Inkomend")] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "stock-notes", children: "Notitie (optioneel)" }), _jsx(Textarea, { id: "stock-notes", rows: 2, value: values.notes ?? "", disabled: !canWrite, onChange: (e) => setValues((v) => ({ ...v, notes: e.target.value })), placeholder: "Bijv. locatie of opmerking" })] }), _jsxs("div", { className: "flex justify-end", children: [!connectionId && (_jsx("span", { className: "mr-auto self-center text-xs text-muted-foreground", children: LOCAL_STOCK_TEXT })), _jsx(Button, { variant: "outline", className: "mr-2", onClick: () => setAfboekOpen(true), disabled: saving || !canWrite || Number(row.physical_stock) <= 0, children: "Afboeken" }), _jsxs(Button, { onClick: handleSave, disabled: saving || !canWrite, children: [saving ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Voorraad opslaan"] })] })] })) })] }), _jsx(Dialog, { open: afboekOpen, onOpenChange: setAfboekOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Afboeken" }), _jsxs(DialogDescription, { children: ["Haal planten van de liggende voorraad (", Number(row.physical_stock), "). Kies altijd een reden."] })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "afboek-aantal", children: "Aantal" }), _jsx(Input, { id: "afboek-aantal", type: "number", min: 1, value: afboekAantal, onChange: (e) => setAfboekAantal(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Reden" }), _jsxs(Select, { value: afboekReden ?? undefined, onValueChange: setAfboekReden, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een reden" }) }), _jsx(SelectContent, { children: AFBOEK_REDENEN.map((r) => (_jsx(SelectItem, { value: r.value, children: r.label }, r.value))) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "afboek-notitie", children: "Notitie (optioneel)" }), _jsx(Textarea, { id: "afboek-notitie", rows: 2, value: afboekNotitie, onChange: (e) => setAfboekNotitie(e.target.value) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setAfboekOpen(false), children: "Annuleren" }), _jsxs(Button, { onClick: handleAfboeken, disabled: afboekBezig || !afboekReden || !afboekAantal, children: [afboekBezig && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Afboeken"] })] })] }) }), _jsx(FloridaySentCard, { connectionId: connectionId, productId: productId, tradeItemId: tradeItemId, productType: isGroupHead ? "GROUP" : hasBom ? "SET" : null })] }));
132
+ return (_jsxs("div", { className: "space-y-4", children: [_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(Package, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Voorraad" }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["Beschikbaar: ", _jsx("span", { className: "tabular-nums font-medium text-foreground", children: available })] })] }), _jsx(CardContent, { className: "pt-0 space-y-4", 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" }), " Voorraad laden\u2026"] })) : (_jsxs(_Fragment, { children: [Number(values.reserved_stock) > Number(values.physical_stock) && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: ["Let op: er is meer gereserveerd (", Number(values.reserved_stock), ") dan er ligt (", Number(values.physical_stock), "). Controleer de open orders van dit artikel."] })), _jsxs("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-4", children: [numField("physical_stock", "Liggend"), numField("minimum_stock", "Minimum"), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "reserved_stock", children: "Gereserveerd" }), _jsx(Input, { id: "reserved_stock", type: "number", readOnly: true, disabled: true, value: String(values.reserved_stock ?? 0) }), _jsx("p", { className: "text-[11px] text-muted-foreground", children: "Berekend uit open orders" })] }), numField("incoming_stock", "Inkomend")] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "stock-change-note", children: "Notitie bij deze wijziging (optioneel)" }), _jsx(Textarea, { id: "stock-change-note", rows: 2, value: wijzigingNotitie, disabled: !canWrite, onChange: (e) => setWijzigingNotitie(e.target.value), placeholder: "Komt alleen in de geschiedenis bij deze wijziging" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("button", { type: "button", className: "text-xs text-muted-foreground underline-offset-2 hover:underline", onClick: () => setArtikelNotitieOpen((o) => !o), children: artikelNotitieOpen ? "Vaste artikelnotitie verbergen" : `Vaste artikelnotitie${values.notes ? " (ingevuld)" : ""}` }), artikelNotitieOpen && (_jsx(Textarea, { id: "stock-notes", rows: 2, value: values.notes ?? "", disabled: !canWrite, onChange: (e) => {
133
+ setArtikelNotitieGewijzigd(true);
134
+ setValues((v) => ({ ...v, notes: e.target.value }));
135
+ }, placeholder: "Bijv. vaste locatie; verandert alleen als je hem hier aanpast" }))] }), _jsxs("div", { className: "flex justify-end", children: [!connectionId && (_jsx("span", { className: "mr-auto self-center text-xs text-muted-foreground", children: LOCAL_STOCK_TEXT })), _jsx(Button, { variant: "outline", className: "mr-2", onClick: () => setAfboekOpen(true), disabled: saving || !canWrite || Number(row.physical_stock) <= 0, children: "Afboeken" }), _jsxs(Button, { onClick: handleSave, disabled: saving || !canWrite, children: [saving ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Voorraad opslaan"] })] })] })) })] }), _jsx(Dialog, { open: afboekOpen, onOpenChange: setAfboekOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Afboeken" }), _jsxs(DialogDescription, { children: ["Haal planten van de liggende voorraad (", Number(row.physical_stock), "). Kies altijd een reden."] })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "afboek-aantal", children: "Aantal" }), _jsx(Input, { id: "afboek-aantal", type: "number", min: 1, value: afboekAantal, onChange: (e) => setAfboekAantal(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Reden" }), _jsxs(Select, { value: afboekReden ?? undefined, onValueChange: setAfboekReden, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een reden" }) }), _jsx(SelectContent, { children: AFBOEK_REDENEN.map((r) => (_jsx(SelectItem, { value: r.value, children: r.label }, r.value))) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "afboek-notitie", children: "Notitie (optioneel)" }), _jsx(Textarea, { id: "afboek-notitie", rows: 2, value: afboekNotitie, onChange: (e) => setAfboekNotitie(e.target.value) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setAfboekOpen(false), children: "Annuleren" }), _jsxs(Button, { onClick: handleAfboeken, disabled: afboekBezig || !afboekReden || !afboekAantal, children: [afboekBezig && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Afboeken"] })] })] }) })] }));
168
136
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Zelfherstel bij het openen van Voorraad: vult ontbrekende standaardgegevens
3
+ * aan via voorraad_ensure_defaults(). Eén keer per sessie, op de achtergrond;
4
+ * een fout blokkeert niets en geeft alleen een melding in gewone taal.
5
+ */
6
+ export declare function useEnsureVoorraadDefaults(): void;
7
+ //# sourceMappingURL=useEnsureVoorraadDefaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEnsureVoorraadDefaults.d.ts","sourceRoot":"","sources":["../../src/hooks/useEnsureVoorraadDefaults.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,yBAAyB,SAaxC"}
@@ -0,0 +1,25 @@
1
+ import { useEffect } from "react";
2
+ import { toast } from "@flowselections/core";
3
+ import { supabase } from "../integrations/supabase/client.js";
4
+ let gestart = false;
5
+ /**
6
+ * Zelfherstel bij het openen van Voorraad: vult ontbrekende standaardgegevens
7
+ * aan via voorraad_ensure_defaults(). Eén keer per sessie, op de achtergrond;
8
+ * een fout blokkeert niets en geeft alleen een melding in gewone taal.
9
+ */
10
+ export function useEnsureVoorraadDefaults() {
11
+ useEffect(() => {
12
+ if (gestart)
13
+ return;
14
+ gestart = true;
15
+ void (async () => {
16
+ const { data, error } = await supabase.rpc("voorraad_ensure_defaults");
17
+ if (error || (data && data.ok === false)) {
18
+ // Functie nog niet geïnstalleerd of mislukt: niets blokkeren.
19
+ if (error?.code === "PGRST202")
20
+ return;
21
+ toast.error("Standaardgegevens van Voorraad konden niet worden aangevuld. Neem contact op met de beheerder als dit blijft gebeuren.");
22
+ }
23
+ })();
24
+ }, []);
25
+ }
@@ -40,7 +40,10 @@ export declare function lookupStock(map: Map<string, TradeItemStockRow>, params:
40
40
  export declare function useTradeItemStock(connectionId: string | null, keyOrKeys: string | string[] | null, productId?: string | null): {
41
41
  row: TradeItemStockRow;
42
42
  loading: boolean;
43
- save: (values: TradeItemStockRow) => Promise<void>;
43
+ save: (values: TradeItemStockRow, opts?: {
44
+ wijzigingNotitie?: string | null;
45
+ artikelNotitieWijzigen?: boolean;
46
+ }) => Promise<Record<string, unknown>>;
44
47
  error: string | null;
45
48
  resolvedKey: string | null;
46
49
  resolvedConnectionId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"useTradeItemStock.d.ts","sourceRoot":"","sources":["../../src/hooks/useTradeItemStock.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAUD,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,GAAG,IAAI,CAEhB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,EAAE,CAUX;AAGD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACnC,MAAM,EAAE;IACN,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,iBAAiB,GAAG,SAAS,CAa/B;AAGD,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EACnC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;;;mBA0GR,iBAAiB;;;;;EA6FnC;AAED,wBAAsB,aAAa,CACjC,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAuBzC"}
1
+ {"version":3,"file":"useTradeItemStock.d.ts","sourceRoot":"","sources":["../../src/hooks/useTradeItemStock.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAUD,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,GAAG,IAAI,CAEhB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,EAAE,CAUX;AAGD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACnC,MAAM,EAAE;IACN,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,iBAAiB,GAAG,SAAS,CAa/B;AAGD,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EACnC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;;;mBA2Gb,iBAAiB,SACnB;QAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,sBAAsB,CAAC,EAAE,OAAO,CAAA;KAAE,KAC3E,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;;;;;EA+EtC;AAED,wBAAsB,aAAa,CACjC,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAuBzC"}
@@ -147,57 +147,43 @@ export function useTradeItemStock(connectionId, keyOrKeys, productId) {
147
147
  supabase.removeChannel(channel);
148
148
  };
149
149
  }, [connectionId, keys, primaryKey, refetch, resolvedConnectionId, resolvedKey]);
150
- const save = useCallback(async (values) => {
150
+ const save = useCallback(async (values, opts = {}) => {
151
151
  let savedKey = resolvedKey ?? primaryKey;
152
152
  let savedConnectionId = resolvedConnectionId ?? connectionId;
153
- if (productId) {
154
- const { data, error: rpcError } = await supabase.rpc("set_product_stock", {
155
- p_product_id: productId,
156
- p_physical_stock: values.physical_stock,
157
- p_minimum_stock: values.minimum_stock,
158
- p_incoming_stock: values.incoming_stock,
159
- // Notitie altijd expliciet meegeven (huidige of nieuwe waarde); minimum
160
- // en inkomend komen uit de geladen rij, dus nooit stil 0.
161
- p_notes: values.notes ?? null,
162
- p_change_source: "MANUAL_STOCK",
163
- });
164
- if (rpcError)
165
- throw new Error(rpcError.message);
166
- const saved = Array.isArray(data) ? data[0] : null;
167
- savedKey = saved?.trade_item_key ?? savedKey;
168
- savedConnectionId = saved?.connection_id ?? savedConnectionId;
169
- setResolvedKey(savedKey);
170
- setResolvedConnectionId(savedConnectionId);
171
- }
172
- else {
173
- if (!connectionId || !primaryKey) {
174
- throw new Error("Ontbrekende connectie of artikel-key");
175
- }
176
- const userRes = await supabase.auth.getUser();
177
- const userId = userRes.data.user?.id;
178
- if (!userId)
179
- throw new Error("Niet ingelogd");
180
- const { error: upsertError } = await supabase
181
- .from("trade_item_stock")
182
- .upsert({
183
- user_id: userId,
184
- connection_id: connectionId,
185
- trade_item_key: primaryKey,
186
- physical_stock: values.physical_stock,
187
- minimum_stock: values.minimum_stock,
188
- // Gereserveerd wordt berekend uit de orders; nooit vanuit dit scherm schrijven.
189
- incoming_stock: values.incoming_stock,
190
- notes: values.notes,
191
- change_source: "MANUAL_STOCK",
192
- }, { onConflict: "connection_id,trade_item_key" });
193
- if (upsertError)
194
- throw new Error(upsertError.message);
153
+ if (!productId && (!connectionId || !primaryKey)) {
154
+ throw new Error("Ontbrekende connectie of artikel-key");
195
155
  }
156
+ // 1.0.140: één opslagfunctie; het antwoord bevestigt dat de database
157
+ // heeft opgeslagen. Notitie per wijziging gaat alleen naar de geschiedenis.
158
+ const { data, error: rpcError } = await supabase.rpc("voorraad_voorraad_opslaan", {
159
+ p_product_id: productId ?? null,
160
+ p_connection_id: productId ? null : connectionId,
161
+ p_trade_item_key: productId ? null : primaryKey,
162
+ p_physical_stock: values.physical_stock,
163
+ p_minimum_stock: values.minimum_stock,
164
+ p_incoming_stock: values.incoming_stock,
165
+ p_wijziging_notitie: opts.wijzigingNotitie?.trim() || null,
166
+ p_artikel_notitie: values.notes ?? null,
167
+ p_artikel_notitie_wijzigen: !!opts.artikelNotitieWijzigen,
168
+ });
169
+ if (rpcError)
170
+ throw rpcError;
171
+ const result = (data ?? null);
172
+ if (!result || result.opgeslagen !== true)
173
+ throw new Error("Opslaan niet bevestigd");
174
+ savedKey = result.trade_item_key ?? savedKey;
175
+ savedConnectionId = result.connection_id ?? savedConnectionId;
176
+ setResolvedKey(savedKey);
177
+ setResolvedConnectionId(savedConnectionId);
196
178
  if (!savedConnectionId || !savedKey) {
197
179
  throw new Error("Ontbrekende connectie of artikel-key");
198
180
  }
199
181
  const previousPhysical = row?.physical_stock ?? 0;
200
- setRow({ ...values, reserved_stock: row?.reserved_stock ?? values.reserved_stock });
182
+ setRow({
183
+ ...values,
184
+ notes: result.notes ?? null,
185
+ reserved_stock: Number(result.reserved_stock ?? row?.reserved_stock ?? values.reserved_stock),
186
+ });
201
187
  // Verkoop = daling van de fysieke voorraad. Het bijbehorende aantal
202
188
  // fusten wordt automatisch van de fustenvoorraad afgeschreven op basis
203
189
  // van de primaire belading van dit artikel.
@@ -234,6 +220,7 @@ export function useTradeItemStock(connectionId, keyOrKeys, productId) {
234
220
  // Engine-tabel niet bereikbaar; voorraad is lokaal wel opgeslagen.
235
221
  }
236
222
  })();
223
+ return result;
237
224
  }, [connectionId, primaryKey, productId, resolvedConnectionId, resolvedKey, row]);
238
225
  return { row: row ?? EMPTY, loading, save, error, resolvedKey, resolvedConnectionId, refetch };
239
226
  }