@flowselections/floriday-voorraad 1.0.137 → 1.0.141

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 (28) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.js +9 -7
  3. package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts.map +1 -1
  4. package/dist-lib/components/voorraad/StockMutationHistoryCard.js +19 -4
  5. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  6. package/dist-lib/components/voorraad/VoorraadPage.js +2 -5
  7. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
  8. package/dist-lib/components/voorraad/VoorraadStockCard.js +69 -47
  9. package/dist-lib/hooks/useTradeItemStock.d.ts +6 -1
  10. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
  11. package/dist-lib/hooks/useTradeItemStock.js +32 -44
  12. package/dist-lib/integrations/supabase/types.d.ts +416 -0
  13. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  14. package/dist-lib/lib/__tests__/release-140.test.d.ts +2 -0
  15. package/dist-lib/lib/__tests__/release-140.test.d.ts.map +1 -0
  16. package/dist-lib/lib/__tests__/release-140.test.js +42 -0
  17. package/dist-lib/lib/__tests__/stock-save.test.d.ts +2 -0
  18. package/dist-lib/lib/__tests__/stock-save.test.d.ts.map +1 -0
  19. package/dist-lib/lib/__tests__/stock-save.test.js +35 -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/stock-save.d.ts +43 -0
  24. package/dist-lib/lib/stock-save.d.ts.map +1 -0
  25. package/dist-lib/lib/stock-save.js +45 -0
  26. package/package.json +3 -1
  27. package/sql/AGENTS.md +3 -0
  28. package/sql/floriday-voorraad-schema.sql +301 -77
@@ -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,+BA+3EL"}
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"}
@@ -1908,13 +1908,15 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1908
1908
  }
1909
1909
  }, [persistAndPush, data]);
1910
1910
  if (connLoading || draftLoading || publishedLoading || productLoading) {
1911
- return (_jsx("main", { className: "p-6", children: _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
1912
- ? "Artikel laden..."
1913
- : draftLoading
1914
- ? "Lokaal artikel laden..."
1915
- : productLoading
1916
- ? "Product laden..."
1917
- : "Floriday-koppeling laden..."] }) }));
1911
+ // Voorraadblok direct bruikbaar; Floriday-gegevens laden op de achtergrond (1.0.138).
1912
+ 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
1914
+ ? "Artikel laden..."
1915
+ : draftLoading
1916
+ ? "Lokaal artikel laden..."
1917
+ : productLoading
1918
+ ? "Product laden..."
1919
+ : "Floriday-koppeling laden..."] })] }));
1918
1920
  }
1919
1921
  if (!activeConnection) {
1920
1922
  return (_jsx("main", { className: "p-6", children: _jsx(Card, { className: "p-6", children: "Geen actieve Floriday-koppeling gevonden. Voeg er eerst een toe in Instellingen \u203A Floriday." }) }));
@@ -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;AA+CD,wBAAgB,wBAAwB,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,KAAK,+BAyN1E"}
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";
@@ -16,6 +17,7 @@ const TYPE_LABEL = {
16
17
  reservation: { label: "Reservering", tone: "bg-blue-100 text-blue-800 border-blue-200" },
17
18
  reservation_release: { label: "Reservering vrijgegeven", tone: "bg-blue-100 text-blue-800 border-blue-200" },
18
19
  reservation_skipped: { label: "Reservering overgeslagen", tone: "bg-amber-100 text-amber-800 border-amber-200" },
20
+ manual_writeoff: { label: "Afgeboekt (met reden)", tone: "bg-rose-100 text-rose-800 border-rose-200" },
19
21
  manual_correction: { label: "Handmatige correctie", tone: "bg-slate-100 text-slate-700 border-slate-200" },
20
22
  order_sync: { label: "Verwerking verkooporders", tone: "bg-indigo-100 text-indigo-800 border-indigo-200" },
21
23
  import: { label: "Import", tone: "bg-slate-100 text-slate-700 border-slate-200" },
@@ -69,7 +71,7 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
69
71
  }
70
72
  const { data, error } = await supabase
71
73
  .from("stock_mutation_log")
72
- .select("id,created_at,mutation_type,qty_delta,physical_before,physical_after,reserved_before,reserved_after,reason,order_id")
74
+ .select("id,created_at,mutation_type,qty_delta,physical_before,physical_after,reserved_before,reserved_after,reason,order_id,created_by")
73
75
  .in("trade_item_key", [...keys])
74
76
  .order("created_at", { ascending: false })
75
77
  .limit(100);
@@ -90,6 +92,18 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
90
92
  orderNumbers.set(String(o.id), String(o.order_number ?? ""));
91
93
  }
92
94
  }
95
+ // Wie: namen los ophalen uit profiles (geen databaserelatie).
96
+ const userIds = Array.from(new Set((data ?? []).map((r) => r.created_by).filter(Boolean)));
97
+ const userNames = new Map();
98
+ if (userIds.length > 0) {
99
+ const { data: profs } = await supabase
100
+ .from("profiles")
101
+ .select("id, display_name, email")
102
+ .in("id", userIds);
103
+ for (const p of (profs ?? [])) {
104
+ userNames.set(String(p.id), String(p.display_name || p.email || ""));
105
+ }
106
+ }
93
107
  if (cancelled)
94
108
  return;
95
109
  setRows((data ?? []).map((r) => ({
@@ -104,11 +118,12 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
104
118
  reason: r.reason ?? null,
105
119
  order_id: r.order_id ?? null,
106
120
  order_number: r.order_id ? orderNumbers.get(String(r.order_id)) ?? null : null,
121
+ created_by_name: r.created_by ? userNames.get(String(r.created_by)) || "Onbekende gebruiker" : null,
107
122
  })));
108
123
  }
109
124
  catch (e) {
110
125
  if (!cancelled)
111
- setError(e.message);
126
+ setError(dbFoutTekst(e, "Geschiedenis laden"));
112
127
  }
113
128
  };
114
129
  load();
@@ -121,7 +136,7 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
121
136
  supabase.removeChannel(channel);
122
137
  };
123
138
  }, [tradeItemKey, productId]);
124
- 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: "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) => {
125
140
  const meta = TYPE_LABEL[r.mutation_type] ?? {
126
141
  label: r.mutation_type || "Onbekend",
127
142
  tone: "bg-slate-100 text-slate-700 border-slate-200",
@@ -132,6 +147,6 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
132
147
  const resChanged = r.reserved_before !== null &&
133
148
  r.reserved_after !== null &&
134
149
  r.reserved_before !== r.reserved_after;
135
- return (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "whitespace-nowrap px-3 py-2", children: formatMoment(r.created_at) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: `border ${meta.tone}`, children: meta.label }) }), _jsxs("td", { className: `px-3 py-2 text-right tabular-nums font-medium ${r.qty_delta < 0 ? "text-rose-700" : r.qty_delta > 0 ? "text-emerald-700" : ""}`, children: [r.qty_delta > 0 ? "+" : "", r.qty_delta] }), _jsxs("td", { className: "whitespace-nowrap px-3 py-2 tabular-nums text-muted-foreground", children: [physChanged && (_jsxs("div", { children: ["Liggend ", r.physical_before, " \u2192 ", r.physical_after] })), resChanged && (_jsxs("div", { children: ["Gereserveerd ", r.reserved_before, " \u2192 ", r.reserved_after] })), !physChanged && !resChanged && _jsx("span", { children: "\u2014" })] }), _jsx("td", { className: "whitespace-nowrap px-3 py-2", children: r.order_id ? (_jsx("a", { href: `/floriday-verkoop/verkooporders/${r.order_id}`, className: "text-primary hover:underline", children: r.order_number ?? "Order" })) : (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })) }), _jsx("td", { className: "px-3 py-2 text-muted-foreground", children: _jsx(CollapsibleText, { text: r.reason }) })] }, r.id));
150
+ return (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "whitespace-nowrap px-3 py-2", children: formatMoment(r.created_at) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: `border ${meta.tone}`, children: meta.label }) }), _jsxs("td", { className: `px-3 py-2 text-right tabular-nums font-medium ${r.qty_delta < 0 ? "text-rose-700" : r.qty_delta > 0 ? "text-emerald-700" : ""}`, children: [r.qty_delta > 0 ? "+" : "", r.qty_delta] }), _jsxs("td", { className: "whitespace-nowrap px-3 py-2 tabular-nums text-muted-foreground", children: [physChanged && (_jsxs("div", { children: ["Liggend ", r.physical_before, " \u2192 ", r.physical_after] })), resChanged && (_jsxs("div", { children: ["Gereserveerd ", r.reserved_before, " \u2192 ", r.reserved_after] })), !physChanged && !resChanged && _jsx("span", { children: "\u2014" })] }), _jsx("td", { className: "whitespace-nowrap px-3 py-2", children: r.order_id ? (_jsx("a", { href: `/floriday-verkoop/verkooporders/${r.order_id}`, className: "text-primary hover:underline", children: r.order_number ?? "Order" })) : (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })) }), _jsx("td", { className: "whitespace-nowrap px-3 py-2 text-muted-foreground", children: r.created_by_name ?? "Systeem" }), _jsx("td", { className: "px-3 py-2 text-muted-foreground", children: _jsx(CollapsibleText, { text: r.reason }) })] }, r.id));
136
151
  }) })] }), rows.length > 5 && (_jsx("div", { className: "flex justify-center border-t bg-muted/20 px-3 py-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setExpanded((current) => !current), className: "gap-1.5", children: expanded ? (_jsxs(_Fragment, { children: [_jsx(ChevronUp, { className: "h-4 w-4" }), " Inklappen"] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDown, { className: "h-4 w-4" }), " Meer tonen (", rows.length - 5, ")"] })) }) }))] }))] })] }));
137
152
  }
@@ -1 +1 @@
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;AAqF3D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,mBAAmB,GAAG,aAAa,CAAC,CAAC;;;;;;;EAgBzG;AAED,wBAAgB,YAAY,gCA0W3B"}
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"}
@@ -4,7 +4,7 @@ import { useNoPriceProducts } from "../../hooks/useNoPriceProducts.js";
4
4
  import { pageRequestGuard } from "../../lib/request-guard.js";
5
5
  const EMPTY_TRIPS = [];
6
6
  import { Link } from "@tanstack/react-router";
7
- import { Button, Card, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useAuthState, } from "@flowselections/core";
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
10
  import { FloridayConnectionNotice } from "./FloridayConnectionNotice.js";
@@ -12,8 +12,6 @@ import { useVoorraadData } from "../../hooks/useVoorraadData.js";
12
12
  import { StockSyncStatus } from "./StockSyncStatus.js";
13
13
  import { StockIntegrityAlert } from "./StockIntegrityAlert.js";
14
14
  import { SupplyReadScopeAlert } from "./SupplyReadScopeAlert.js";
15
- import { StockPushMonitor } from "./StockPushMonitor.js";
16
- import { FloridayLeadingSummary, FloridayTakeoverPanel } from "./FloridayTakeoverPanel.js";
17
15
  import { ExcelExportButton } from "./ExcelExportButton.js";
18
16
  import { StockBulkButtons } from "./StockBulkButtons.js";
19
17
  import { VoorraadTable } from "./VoorraadTable.js";
@@ -71,7 +69,6 @@ export function countStock(items) {
71
69
  return { total: items.length, inStock, outOfStock, belowMin, fullyReserved, freeAvailable };
72
70
  }
73
71
  export function VoorraadPage() {
74
- const auth = useAuthState();
75
72
  const conn = useFloridayConnection();
76
73
  const { activeId, error: connError } = conn;
77
74
  const { items, loading, error, reload } = useVoorraadData(activeId);
@@ -152,7 +149,7 @@ export function VoorraadPage() {
152
149
  noPriceBreakdown.unit > 0 ? `${noPriceBreakdown.unit} met een andere eenheid dan stuks` : null,
153
150
  ]
154
151
  .filter(Boolean)
155
- .join(", "), "."] }), " ", "Kies \"Toon deze artikelen\", open een artikel en vul bij Artikel regulier de verkoopprijs per stuk (excl. btw) in. De voorraadsturing van elk artikel heeft ook een link \"Prijs invullen\"."] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setNoPriceOnly((v) => !v), children: noPriceOnly ? "Alle artikelen tonen" : "Toon deze artikelen" })] })), _jsx(FloridayLeadingSummary, { connectionId: activeId ?? null }), auth.isAdmin && _jsx(FloridayTakeoverPanel, { connectionId: activeId ?? null }), auth.isAdmin && _jsx(StockPushMonitor, { connectionId: activeId ?? null }), _jsx(FloridayConnectionNotice, { status: conn.status, environment: conn.selectedEnvironment, error: connError }), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock"), hint: "Artikelen waarvan nu iets op de kwekerij staat." }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "primary", label: "Vrij beschikbaar", count: counts.freeAvailable, active: false, hint: "Artikelen waarvan na aftrek van wat voor orders gereserveerd is nog iets over is." }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
152
+ .join(", "), "."] }), " ", "Kies \"Toon deze artikelen\", open een artikel en vul bij Artikel regulier de verkoopprijs per stuk (excl. btw) in. De voorraadsturing van elk artikel heeft ook een link \"Prijs invullen\"."] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setNoPriceOnly((v) => !v), children: noPriceOnly ? "Alle artikelen tonen" : "Toon deze artikelen" })] })), _jsx(FloridayConnectionNotice, { status: conn.status, environment: conn.selectedEnvironment, error: connError }), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock"), hint: "Artikelen waarvan nu iets op de kwekerij staat." }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "primary", label: "Vrij beschikbaar", count: counts.freeAvailable, active: false, hint: "Artikelen waarvan na aftrek van wat voor orders gereserveerd is nog iets over is." }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
156
153
  const active = stockFilter === t.id;
157
154
  return (_jsxs("button", { type: "button", onClick: () => setStockFilter(t.id), className: `relative py-3 px-3 text-sm font-medium whitespace-nowrap transition-colors ${active
158
155
  ? "text-foreground"
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AAgBA,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,sCA4MzF"}
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"}
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
- import { Button, Card, CardContent, Input, Label, Textarea, toast, } from "@flowselections/core";
3
+ import { Button, Card, CardContent, Input, Label, Textarea, toast, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
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";
8
+ import { AFBOEK_REDENEN, afboekFout, opslaanMelding, OPSLAAN_MISLUKT_TEKST, valuesAfterSave } from "../../lib/stock-save.js";
9
+ import { dbFoutTekst } from "../../lib/db-error-message.js";
9
10
  export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productId }) {
10
11
  const keys = stockKeysFor({
11
12
  tradeItemId,
@@ -13,32 +14,18 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
13
14
  productId,
14
15
  });
15
16
  const key = keys[0] ?? null;
16
- const { row, loading, save, error: stockError } = useTradeItemStock(connectionId, keys, productId);
17
+ const { row, loading, save, error: stockError, resolvedKey, resolvedConnectionId, refetch } = useTradeItemStock(connectionId, keys, productId);
17
18
  const [values, setValues] = useState(row);
18
19
  const [saving, setSaving] = useState(false);
19
20
  const [touched, setTouched] = useState(false);
20
- const [hasBom, setHasBom] = useState(false);
21
- // Heeft dit artikel onderdelen (samenstelling)? Bepaalt alleen het label
22
- // in de Floriday-kaart. Produceren bestaat niet meer in deze module.
23
- useEffect(() => {
24
- let cancelled = false;
25
- (async () => {
26
- if (!productId && !tradeItemId) {
27
- setHasBom(false);
28
- return;
29
- }
30
- let query = supabase.from("product_bom_items").select("id").limit(1);
31
- query = productId
32
- ? query.eq("parent_product_id", productId)
33
- : query.eq("parent_trade_item_floriday_id", tradeItemId);
34
- const { data } = await query;
35
- if (!cancelled)
36
- setHasBom((data ?? []).length > 0);
37
- })();
38
- return () => {
39
- cancelled = true;
40
- };
41
- }, [productId, tradeItemId]);
21
+ const [afboekOpen, setAfboekOpen] = useState(false);
22
+ const [afboekAantal, setAfboekAantal] = useState("");
23
+ const [afboekReden, setAfboekReden] = useState(null);
24
+ const [afboekNotitie, setAfboekNotitie] = useState("");
25
+ const [afboekBezig, setAfboekBezig] = useState(false);
26
+ const [wijzigingNotitie, setWijzigingNotitie] = useState("");
27
+ const [artikelNotitieOpen, setArtikelNotitieOpen] = useState(false);
28
+ const [artikelNotitieGewijzigd, setArtikelNotitieGewijzigd] = useState(false);
42
29
  // Is dit artikel de kop van een productgroep? Dan geen voorraad invoeren.
43
30
  const [isGroupHead, setIsGroupHead] = useState(false);
44
31
  useEffect(() => {
@@ -70,45 +57,80 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
70
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." }));
71
58
  }
72
59
  if (stockError) {
73
- 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." }));
74
61
  }
75
62
  const available = Math.max(0, values.physical_stock - values.reserved_stock);
76
- // Foutmeldingen uit de database zijn soms JSON of technisch; hier maken we
77
- // er gewone taal van zodat de gebruiker weet wat er moet gebeuren.
78
- const humanError = (e) => {
79
- const raw = e?.message ?? String(e);
80
- try {
81
- const parsed = JSON.parse(raw);
82
- const msg = parsed?.message ?? parsed?.error ?? raw;
83
- if (typeof msg === "string" && msg.toLowerCase().includes("onvoldoende voorraad")) {
84
- return "Onvoldoende voorraad van een onderdeel. Pas de voorraad van het onderdeel eerst aan.";
85
- }
86
- return typeof msg === "string" ? msg : raw;
87
- }
88
- catch {
89
- return raw;
90
- }
91
- };
92
63
  // Opslaan via set_product_stock werkt ook zonder koppeling (lokale context, 1.0.107).
93
64
  const canWrite = !!productId || !!connectionId;
94
65
  const handleSave = async () => {
95
66
  if (!canWrite)
96
67
  return;
97
68
  setSaving(true);
69
+ let result = null;
70
+ let fout = null;
71
+ try {
72
+ result = await save(values, { wijzigingNotitie, artikelNotitieWijzigen: artikelNotitieGewijzigd });
73
+ }
74
+ catch (e) {
75
+ fout = dbFoutTekst(e, "Opslaan");
76
+ }
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);
88
+ }
89
+ setSaving(false);
90
+ };
91
+ const handleAfboeken = async () => {
92
+ const aantal = Number(afboekAantal);
93
+ const fout = afboekFout(aantal, afboekReden, Number(row.physical_stock));
94
+ if (fout) {
95
+ toast.error(fout);
96
+ return;
97
+ }
98
+ const key2 = resolvedKey ?? key;
99
+ if (!key2) {
100
+ toast.error("Sla eerst de voorraad op voordat je afboekt.");
101
+ return;
102
+ }
103
+ setAfboekBezig(true);
98
104
  try {
99
- await save(values);
100
- toast.success("Voorraad gecorrigeerd");
105
+ const { error } = await supabase.rpc("voorraad_afboeken", {
106
+ p_connection_id: resolvedConnectionId ?? connectionId,
107
+ p_trade_item_key: key2,
108
+ p_aantal: aantal,
109
+ p_reden: afboekReden,
110
+ p_notitie: afboekNotitie.trim() || null,
111
+ });
112
+ if (error)
113
+ throw error;
114
+ toast.success(`${aantal} afgeboekt`);
115
+ setAfboekOpen(false);
116
+ setAfboekAantal("");
117
+ setAfboekReden(null);
118
+ setAfboekNotitie("");
119
+ await refetch();
101
120
  }
102
121
  catch (e) {
103
- toast.error(`Opslaan mislukt: ${humanError(e)}`);
122
+ toast.error(`Afboeken mislukt, niets gewijzigd. ${dbFoutTekst(e, "Afboeken")}`);
104
123
  }
105
124
  finally {
106
- setSaving(false);
125
+ setAfboekBezig(false);
107
126
  }
108
127
  };
109
128
  const numField = (id, label) => (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(Input, { id: id, type: "number", min: 0, disabled: !canWrite, value: String(values[id] ?? 0), onChange: (e) => {
110
129
  setTouched(true);
111
130
  setValues((v) => ({ ...v, [id]: Number(e.target.value) || 0 }));
112
131
  } })] }));
113
- 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"), numField("reserved_stock", "Gereserveerd"), 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 })), _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(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"] })] })] }) })] }));
114
136
  }
@@ -40,9 +40,14 @@ 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;
49
+ resolvedConnectionId: string | null;
50
+ refetch: () => Promise<void>;
46
51
  };
47
52
  export declare function fetchStockMap(connectionId: string | null): Promise<Map<string, TradeItemStockRow>>;
48
53
  //# sourceMappingURL=useTradeItemStock.d.ts.map
@@ -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;;;EA4FnC;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,56 +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
- reserved_stock: values.reserved_stock,
189
- incoming_stock: values.incoming_stock,
190
- notes: values.notes,
191
- }, { onConflict: "connection_id,trade_item_key" });
192
- if (upsertError)
193
- throw new Error(upsertError.message);
153
+ if (!productId && (!connectionId || !primaryKey)) {
154
+ throw new Error("Ontbrekende connectie of artikel-key");
194
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);
195
178
  if (!savedConnectionId || !savedKey) {
196
179
  throw new Error("Ontbrekende connectie of artikel-key");
197
180
  }
198
181
  const previousPhysical = row?.physical_stock ?? 0;
199
- setRow(values);
182
+ setRow({
183
+ ...values,
184
+ notes: result.notes ?? null,
185
+ reserved_stock: Number(result.reserved_stock ?? row?.reserved_stock ?? values.reserved_stock),
186
+ });
200
187
  // Verkoop = daling van de fysieke voorraad. Het bijbehorende aantal
201
188
  // fusten wordt automatisch van de fustenvoorraad afgeschreven op basis
202
189
  // van de primaire belading van dit artikel.
@@ -233,8 +220,9 @@ export function useTradeItemStock(connectionId, keyOrKeys, productId) {
233
220
  // Engine-tabel niet bereikbaar; voorraad is lokaal wel opgeslagen.
234
221
  }
235
222
  })();
223
+ return result;
236
224
  }, [connectionId, primaryKey, productId, resolvedConnectionId, resolvedKey, row]);
237
- return { row: row ?? EMPTY, loading, save, error, resolvedKey };
225
+ return { row: row ?? EMPTY, loading, save, error, resolvedKey, resolvedConnectionId, refetch };
238
226
  }
239
227
  export async function fetchStockMap(connectionId) {
240
228
  // Met koppeling: alleen de rijen van die koppeling. Zonder koppeling: de