@flowselections/floriday-voorraad 1.0.108 → 1.0.113
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.
- package/dist-lib/components/voorraad/FustenPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/FustenPage.js +10 -6
- package/dist-lib/components/voorraad/PriceStatusBadge.d.ts.map +1 -1
- package/dist-lib/components/voorraad/PriceStatusBadge.js +6 -1
- package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadPage.js +8 -37
- package/dist-lib/hooks/useNoPriceProducts.d.ts +10 -0
- package/dist-lib/hooks/useNoPriceProducts.d.ts.map +1 -0
- package/dist-lib/hooks/useNoPriceProducts.js +56 -0
- package/dist-lib/hooks/useStockPushJobs.d.ts.map +1 -1
- package/dist-lib/hooks/useStockPushJobs.js +6 -1
- package/dist-lib/hooks/useStockSyncHealth.d.ts.map +1 -1
- package/dist-lib/hooks/useStockSyncHealth.js +2 -9
- package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
- package/dist-lib/hooks/useVoorraadData.js +45 -24
- package/dist-lib/integrations/supabase/types.d.ts +1049 -122
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/__tests__/fust-dimensions.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/fust-dimensions.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/fust-dimensions.test.js +27 -0
- package/dist-lib/lib/__tests__/request-guard.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/request-guard.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/request-guard.test.js +50 -0
- package/dist-lib/lib/fusten.d.ts +14 -3
- package/dist-lib/lib/fusten.d.ts.map +1 -1
- package/dist-lib/lib/fusten.js +38 -7
- package/dist-lib/lib/request-guard.d.ts +46 -0
- package/dist-lib/lib/request-guard.d.ts.map +1 -0
- package/dist-lib/lib/request-guard.js +100 -0
- package/package.json +1 -1
- package/sql/floriday-voorraad-schema.sql +343 -28
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FustenPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FustenPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FustenPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FustenPage.tsx"],"names":[],"mappings":"AAgLA,wBAAgB,UAAU,gCAiQzB"}
|
|
@@ -5,30 +5,30 @@ import { Button, Card, Input, Badge, toast } from "@flowselections/core";
|
|
|
5
5
|
import { supabase } from "../../integrations/supabase/client.js";
|
|
6
6
|
import { useFloridayConnection } from "../../hooks/useFloridayConnection.js";
|
|
7
7
|
import { useFusten } from "../../hooks/useFusten.js";
|
|
8
|
-
import { fetchFloricodeFustMap, fetchFustMutations, formatFustDimensions, resolveFustDimensions, } from "../../lib/fusten.js";
|
|
8
|
+
import { fetchFloricodeFustMap, fetchFloridayCustomPackageMap, fetchFustMutations, formatFustDimensions, resolveFustDimensions, } from "../../lib/fusten.js";
|
|
9
9
|
/** Afmetingen tonen en handmatig aanvullen wanneer de catalogus ze niet kent. */
|
|
10
|
-
function DimensionCell({ fust, floricode, onSave, }) {
|
|
10
|
+
function DimensionCell({ fust, floricode, customPackages, onSave, }) {
|
|
11
11
|
const [editing, setEditing] = useState(false);
|
|
12
12
|
const [draft, setDraft] = useState({
|
|
13
13
|
length_mm: fust.length_mm?.toString() ?? "",
|
|
14
14
|
width_mm: fust.width_mm?.toString() ?? "",
|
|
15
15
|
height_mm: fust.height_mm?.toString() ?? "",
|
|
16
16
|
});
|
|
17
|
-
const resolved = resolveFustDimensions(fust, floricode);
|
|
17
|
+
const resolved = resolveFustDimensions(fust, floricode, customPackages);
|
|
18
18
|
const label = formatFustDimensions(resolved);
|
|
19
19
|
const toNum = (v) => {
|
|
20
20
|
const n = Math.trunc(Number(v));
|
|
21
21
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
22
22
|
};
|
|
23
23
|
if (!editing) {
|
|
24
|
-
return (_jsxs("div", { children: [_jsx("div", { className: label ? "tabular-nums" : "text-muted-foreground", children: label ?? "Onbekend" }), resolved.source !== "onbekend" && (_jsx("div", { className: "text-xs text-muted-foreground", children: resolved.source === "floricode" ? "Floricode" : "handmatig" })), _jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => {
|
|
24
|
+
return (_jsxs("div", { children: [_jsx("div", { className: label ? "tabular-nums" : "text-muted-foreground", children: label ?? "Onbekend" }), resolved.source !== "onbekend" && (_jsx("div", { className: "text-xs text-muted-foreground", children: resolved.source === "floricode" ? "Floricode" : resolved.source === "floriday" ? "Floriday eigen fust" : "handmatig" })), _jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => {
|
|
25
25
|
setDraft({
|
|
26
26
|
length_mm: fust.length_mm?.toString() ?? "",
|
|
27
27
|
width_mm: fust.width_mm?.toString() ?? "",
|
|
28
28
|
height_mm: fust.height_mm?.toString() ?? "",
|
|
29
29
|
});
|
|
30
30
|
setEditing(true);
|
|
31
|
-
}, children: resolved.source === "handmatig" ? "Wijzigen" : resolved.source === "floricode" ? "Handmatig aanvullen" : "Afmeting toevoegen" })] }));
|
|
31
|
+
}, children: resolved.source === "handmatig" ? "Wijzigen" : resolved.source === "floricode" || resolved.source === "floriday" ? "Handmatig aanvullen" : "Afmeting toevoegen" })] }));
|
|
32
32
|
}
|
|
33
33
|
return (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1", children: [["length_mm", "width_mm", "height_mm"].map((k, i) => (_jsxs("div", { className: "flex items-center gap-1", children: [i > 0 && _jsx("span", { className: "text-muted-foreground", children: "\u00D7" }), _jsx(Input, { type: "number", className: "h-8 w-20 text-right", placeholder: ["L", "B", "H"][i], value: draft[k], onChange: (e) => setDraft((d) => ({ ...d, [k]: e.target.value })) })] }, k))), _jsx("span", { className: "text-xs text-muted-foreground", children: "mm" })] }), _jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { size: "sm", className: "h-7", onClick: async () => {
|
|
34
34
|
await onSave({
|
|
@@ -73,7 +73,11 @@ export function FustenPage() {
|
|
|
73
73
|
fetchFloricodeFustMap()
|
|
74
74
|
.then((m) => { setFloricode(m); setFloricodeError(null); })
|
|
75
75
|
.catch((e) => setFloricodeError(e.message));
|
|
76
|
+
fetchFloridayCustomPackageMap()
|
|
77
|
+
.then(setCustomPackages)
|
|
78
|
+
.catch(() => setCustomPackages(new Map()));
|
|
76
79
|
}, []);
|
|
80
|
+
const [customPackages, setCustomPackages] = useState(null);
|
|
77
81
|
const [busyId, setBusyId] = useState(null);
|
|
78
82
|
const [amounts, setAmounts] = useState({});
|
|
79
83
|
const [negative, setNegative] = useState([]);
|
|
@@ -115,7 +119,7 @@ export function FustenPage() {
|
|
|
115
119
|
void loadNegative();
|
|
116
120
|
}
|
|
117
121
|
};
|
|
118
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-start gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight truncate", children: "Fusten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Alle fusttypen worden automatisch afgeleid uit de beladingen van je producten." })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void sync(), disabled: syncing, children: [_jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${syncing ? "animate-spin" : ""}` }), "Synchroniseren"] })] }), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Fusten laden mislukt: ", error] })), floricodeError && (_jsxs("div", { className: "rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm", children: ["Floricode-fustcodes niet leesbaar: ", floricodeError, ". Alleen handmatig ingevoerde maten worden getoond."] })), negative.length > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive space-y-1", children: [_jsxs("p", { className: "font-medium", children: [negative.length, " fusttype", negative.length === 1 ? " heeft" : "n hebben", " een tekort."] }), _jsx("p", { className: "text-foreground/80", children: "Er is meer uitgeleverd dan ingeboekt. Controleer de ontvangsten en retouren, of boek de werkelijke telling bij." }), _jsx("ul", { className: "text-xs text-foreground/80", children: negative.map((n) => (_jsxs("li", { children: [n.name.trim(), ": tekort van ", n.shortage.toLocaleString("nl-NL"), " fusten", n.duplicate_names > 0 ? " (dit fusttype bestaat meerdere keren)" : ""] }, n.fust_type_id))) })] })), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs(Card, { className: "p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Fusttypen" }), _jsx("p", { className: "mt-2 text-3xl font-bold tabular-nums", children: totals.types })] }), _jsxs(Card, { className: "p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Totale fustvoorraad" }), _jsx("p", { className: "mt-2 text-3xl font-bold tabular-nums", children: totals.stock })] }), _jsxs(Card, { className: "p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Onder minimum" }), _jsx("p", { className: "mt-2 text-3xl font-bold tabular-nums", children: totals.belowMin })] })] }), _jsxs("div", { className: "relative max-w-sm", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Zoek fust\u2026", className: "pl-9" })] }), _jsx(Card, { className: "overflow-hidden", children: loading ? (_jsx("p", { className: "p-6 text-sm text-muted-foreground", children: "Fusten laden\u2026" })) : filtered.length === 0 ? (_jsxs("div", { className: "p-6 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Boxes, { className: "h-4 w-4" }), "Nog geen fusttypen gevonden. Voeg een belading toe aan een product."] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3", children: "Fust" }), _jsx("th", { className: "p-3", children: "Bron" }), _jsx("th", { className: "p-3", children: "Afmeting (L \u00D7 B \u00D7 H)" }), _jsx("th", { className: "p-3 text-right", children: "Gebruikt door" }), _jsx("th", { className: "p-3 text-right", children: "Voorraad" }), _jsx("th", { className: "p-3 text-right", children: "Minimum" }), _jsx("th", { className: "p-3 text-right", children: "Aanpassen" })] }) }), _jsx("tbody", { children: filtered.map((f) => (_jsxs("tr", { className: "border-t border-border align-top", children: [_jsxs("td", { className: "p-3", children: [_jsx("div", { className: "font-medium", children: f.name }), _jsx("div", { className: "text-xs text-muted-foreground", children: f.vbn_code ? `VBN ${f.vbn_code}` : f.fust_key }), !f.in_use && (_jsx(Badge, { variant: "outline", className: "mt-1", children: "Niet meer in gebruik" })), _jsx("div", { className: "mt-1", children: _jsx(MutationHistory, { fust: f }) })] }), _jsx("td", { className: "p-3", children: f.source === "SUPPLIER" ? "Leverancier" : "VBN" }), _jsx("td", { className: "p-3 text-sm", children: _jsx(DimensionCell, { fust: f, floricode: floricode, onSave: (v) => patch(f.id, v) }) }), _jsx("td", { className: "p-3 text-right tabular-nums", children: f.used_by_count }), _jsx("td", { className: "p-3 text-right", children: f.stock < 0 ? (_jsxs("span", { className: "tabular-nums font-semibold text-rose-600", title: "Er is meer uitgeleverd dan ingeboekt. Boek ontvangen of geretourneerde fusten bij.", children: ["Tekort van ", Math.abs(f.stock).toLocaleString("nl-NL"), " fusten"] })) : (_jsx("span", { className: `tabular-nums font-semibold ${f.minimum_stock > 0 && f.stock < f.minimum_stock
|
|
122
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-start gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight truncate", children: "Fusten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Alle fusttypen worden automatisch afgeleid uit de beladingen van je producten." })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void sync(), disabled: syncing, children: [_jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${syncing ? "animate-spin" : ""}` }), "Synchroniseren"] })] }), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Fusten laden mislukt: ", error] })), floricodeError && (_jsxs("div", { className: "rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm", children: ["Floricode-fustcodes niet leesbaar: ", floricodeError, ". Alleen handmatig ingevoerde maten worden getoond."] })), negative.length > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive space-y-1", children: [_jsxs("p", { className: "font-medium", children: [negative.length, " fusttype", negative.length === 1 ? " heeft" : "n hebben", " een tekort."] }), _jsx("p", { className: "text-foreground/80", children: "Er is meer uitgeleverd dan ingeboekt. Controleer de ontvangsten en retouren, of boek de werkelijke telling bij." }), _jsx("ul", { className: "text-xs text-foreground/80", children: negative.map((n) => (_jsxs("li", { children: [n.name.trim(), ": tekort van ", n.shortage.toLocaleString("nl-NL"), " fusten", n.duplicate_names > 0 ? " (dit fusttype bestaat meerdere keren)" : ""] }, n.fust_type_id))) })] })), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs(Card, { className: "p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Fusttypen" }), _jsx("p", { className: "mt-2 text-3xl font-bold tabular-nums", children: totals.types })] }), _jsxs(Card, { className: "p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Totale fustvoorraad" }), _jsx("p", { className: "mt-2 text-3xl font-bold tabular-nums", children: totals.stock })] }), _jsxs(Card, { className: "p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Onder minimum" }), _jsx("p", { className: "mt-2 text-3xl font-bold tabular-nums", children: totals.belowMin })] })] }), _jsxs("div", { className: "relative max-w-sm", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Zoek fust\u2026", className: "pl-9" })] }), _jsx(Card, { className: "overflow-hidden", children: loading ? (_jsx("p", { className: "p-6 text-sm text-muted-foreground", children: "Fusten laden\u2026" })) : filtered.length === 0 ? (_jsxs("div", { className: "p-6 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Boxes, { className: "h-4 w-4" }), "Nog geen fusttypen gevonden. Voeg een belading toe aan een product."] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3", children: "Fust" }), _jsx("th", { className: "p-3", children: "Bron" }), _jsx("th", { className: "p-3", children: "Afmeting (L \u00D7 B \u00D7 H)" }), _jsx("th", { className: "p-3 text-right", children: "Gebruikt door" }), _jsx("th", { className: "p-3 text-right", children: "Voorraad" }), _jsx("th", { className: "p-3 text-right", children: "Minimum" }), _jsx("th", { className: "p-3 text-right", children: "Aanpassen" })] }) }), _jsx("tbody", { children: filtered.map((f) => (_jsxs("tr", { className: "border-t border-border align-top", children: [_jsxs("td", { className: "p-3", children: [_jsx("div", { className: "font-medium", children: f.name }), _jsx("div", { className: "text-xs text-muted-foreground", children: f.vbn_code ? `VBN ${f.vbn_code}` : f.fust_key }), !f.in_use && (_jsx(Badge, { variant: "outline", className: "mt-1", children: "Niet meer in gebruik" })), _jsx("div", { className: "mt-1", children: _jsx(MutationHistory, { fust: f }) })] }), _jsx("td", { className: "p-3", children: f.source === "SUPPLIER" ? "Leverancier" : "VBN" }), _jsx("td", { className: "p-3 text-sm", children: _jsx(DimensionCell, { fust: f, floricode: floricode, customPackages: customPackages, onSave: (v) => patch(f.id, v) }) }), _jsx("td", { className: "p-3 text-right tabular-nums", children: f.used_by_count }), _jsx("td", { className: "p-3 text-right", children: f.stock < 0 ? (_jsxs("span", { className: "tabular-nums font-semibold text-rose-600", title: "Er is meer uitgeleverd dan ingeboekt. Boek ontvangen of geretourneerde fusten bij.", children: ["Tekort van ", Math.abs(f.stock).toLocaleString("nl-NL"), " fusten"] })) : (_jsx("span", { className: `tabular-nums font-semibold ${f.minimum_stock > 0 && f.stock < f.minimum_stock
|
|
119
123
|
? "text-rose-600"
|
|
120
124
|
: ""}`, children: f.stock })) }), _jsx("td", { className: "p-3 text-right", children: _jsx(Input, { type: "number", className: "h-8 w-28 text-right", defaultValue: f.minimum_stock, onBlur: (e) => {
|
|
121
125
|
const v = Math.max(0, Math.trunc(Number(e.target.value) || 0));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PriceStatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/PriceStatusBadge.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PriceStatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/PriceStatusBadge.tsx"],"names":[],"mappings":"AA2FA,KAAK,KAAK,GAAG;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAeF,wBAAgB,gBAAgB,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,KAAK,sCAmG/E"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isTabHidden } from "../../lib/request-guard.js";
|
|
2
3
|
import { CircleDollarSign, Clock, MinusCircle, Loader2 } from "lucide-react";
|
|
3
4
|
import { useEffect, useState } from "react";
|
|
4
5
|
import { useTradeItemSupplyLine } from "../../hooks/useTradeItemSupplyLine.js";
|
|
@@ -36,7 +37,11 @@ function useLastSentPrice(tradeItemId, productId) {
|
|
|
36
37
|
: null);
|
|
37
38
|
};
|
|
38
39
|
void load();
|
|
39
|
-
|
|
40
|
+
// Alleen opvragen als het tabblad zichtbaar is (1.0.111).
|
|
41
|
+
const t = setInterval(() => {
|
|
42
|
+
if (!isTabHidden())
|
|
43
|
+
void load();
|
|
44
|
+
}, 30000);
|
|
40
45
|
return () => {
|
|
41
46
|
cancelled = true;
|
|
42
47
|
clearInterval(t);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AA+GA,wBAAgB,YAAY,gCA2W3B"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useNoPriceProducts } from "../../hooks/useNoPriceProducts.js";
|
|
4
|
+
import { pageRequestGuard } from "../../lib/request-guard.js";
|
|
5
|
+
const EMPTY_TRIPS = [];
|
|
3
6
|
import { Link } from "@tanstack/react-router";
|
|
4
7
|
import { Button, Card, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useAuthState, } from "@flowselections/core";
|
|
5
8
|
import { AlertTriangle, CheckCircle2, FileText, PackageX, Plus, Search, } from "lucide-react";
|
|
6
9
|
import { useFloridayConnection } from "../../hooks/useFloridayConnection.js";
|
|
7
10
|
import { FloridayConnectionNotice } from "./FloridayConnectionNotice.js";
|
|
8
|
-
import { supabase } from "../../integrations/supabase/client.js";
|
|
9
11
|
import { useVoorraadData } from "../../hooks/useVoorraadData.js";
|
|
10
12
|
import { StockSyncStatus } from "./StockSyncStatus.js";
|
|
11
13
|
import { StockIntegrityAlert } from "./StockIntegrityAlert.js";
|
|
@@ -58,41 +60,10 @@ export function VoorraadPage() {
|
|
|
58
60
|
const [drawerIds, setDrawerIds] = useState([]);
|
|
59
61
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
60
62
|
// Artikelen zonder geldige prijs per stuk (eenheid "stuks", prijs > 0), op id.
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
+
// Los van de identiteit van `items`: dat effect veroorzaakte een opvraag-lus (1.0.111).
|
|
64
|
+
const { noPriceIds, noPriceBreakdown } = useNoPriceProducts();
|
|
63
65
|
const [noPriceOnly, setNoPriceOnly] = useState(false);
|
|
64
|
-
|
|
65
|
-
let cancelled = false;
|
|
66
|
-
void supabase
|
|
67
|
-
.from("products")
|
|
68
|
-
.select("id, unit, sale_price")
|
|
69
|
-
.then(({ data }) => {
|
|
70
|
-
if (cancelled)
|
|
71
|
-
return;
|
|
72
|
-
const s = new Set();
|
|
73
|
-
const b = { empty: 0, zero: 0, unit: 0 };
|
|
74
|
-
for (const p of (data ?? [])) {
|
|
75
|
-
const isPiece = (p.unit ?? "").trim().toLowerCase() === "stuks";
|
|
76
|
-
if (!isPiece) {
|
|
77
|
-
s.add(p.id);
|
|
78
|
-
b.unit++;
|
|
79
|
-
}
|
|
80
|
-
else if (p.sale_price == null) {
|
|
81
|
-
s.add(p.id);
|
|
82
|
-
b.empty++;
|
|
83
|
-
}
|
|
84
|
-
else if (!(Number(p.sale_price) > 0)) {
|
|
85
|
-
s.add(p.id);
|
|
86
|
-
b.zero++;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
setNoPriceIds(s);
|
|
90
|
-
setNoPriceBreakdown(b);
|
|
91
|
-
});
|
|
92
|
-
return () => {
|
|
93
|
-
cancelled = true;
|
|
94
|
-
};
|
|
95
|
-
}, [items]);
|
|
66
|
+
const guardTrips = useSyncExternalStore(pageRequestGuard.subscribe, () => pageRequestGuard.trips(), () => EMPTY_TRIPS);
|
|
96
67
|
const toggleSelect = (id) => setSelected((cur) => {
|
|
97
68
|
const next = new Set(cur);
|
|
98
69
|
if (next.has(id))
|
|
@@ -162,7 +133,7 @@ export function VoorraadPage() {
|
|
|
162
133
|
{ id: "out_of_stock", label: "Niet op voorraad", count: counts.outOfStock },
|
|
163
134
|
{ id: "below_min", label: "Onder minimum", count: counts.belowMin },
|
|
164
135
|
];
|
|
165
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-[12rem] flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _jsx(StockIntegrityAlert, {}), _jsx(SupplyReadScopeAlert, {}), _jsx(StockSyncStatus, {}), noPriceIds.size > 0 && (_jsxs("div", { className: "flex flex-wrap items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-800", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("span", { className: "font-medium", children: [noPriceIds.size, " artikel", noPriceIds.size === 1 ? " staat" : "en staan", " niet in Floriday zonder geldige prijs:", " ", [
|
|
136
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-[12rem] flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), guardTrips.length > 0 && (_jsxs("div", { role: "alert", className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: ["Deze pagina vroeg te vaak gegevens op en is daarmee gestopt. Open de pagina opnieuw.", _jsxs("details", { className: "text-xs opacity-80", children: [_jsx("summary", { className: "cursor-pointer", children: "Details voor beheer" }), guardTrips.map((t) => (_jsxs("div", { children: [t.table, ": ", t.count, " aanvragen binnen 10 s om ", new Date(t.at).toLocaleTimeString("nl-NL")] }, t.table)))] })] })), _jsx(StockIntegrityAlert, {}), _jsx(SupplyReadScopeAlert, {}), _jsx(StockSyncStatus, {}), noPriceIds.size > 0 && (_jsxs("div", { className: "flex flex-wrap items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-800", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("span", { className: "font-medium", children: [noPriceIds.size, " artikel", noPriceIds.size === 1 ? " staat" : "en staan", " niet in Floriday zonder geldige prijs:", " ", [
|
|
166
137
|
noPriceBreakdown.empty > 0 ? `${noPriceBreakdown.empty} zonder verkoopprijs` : null,
|
|
167
138
|
noPriceBreakdown.zero > 0 ? `${noPriceBreakdown.zero} met prijs € 0,00` : null,
|
|
168
139
|
noPriceBreakdown.unit > 0 ? `${noPriceBreakdown.unit} met een andere eenheid dan stuks` : null,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNoPriceProducts.d.ts","sourceRoot":"","sources":["../../src/hooks/useNoPriceProducts.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,wBAAgB,kBAAkB;;;EA+CjC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artikelen zonder geldige prijs per stuk. Eén keer bij openen, daarna alleen na een
|
|
3
|
+
* echte wijziging aan products (realtime), gebundeld tot hooguit 1 aanvraag per 5 s.
|
|
4
|
+
* Bewust los van de identiteit van de voorraadlijst (die veroorzaakte een opvraag-lus).
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { supabase } from "../integrations/supabase/client.js";
|
|
8
|
+
import { createBundler, pageRequestGuard } from "../lib/request-guard.js";
|
|
9
|
+
export function useNoPriceProducts() {
|
|
10
|
+
const [ids, setIds] = useState(new Set());
|
|
11
|
+
const [breakdown, setBreakdown] = useState({ empty: 0, zero: 0, unit: 0 });
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
const load = async () => {
|
|
15
|
+
if (!pageRequestGuard.allow("products"))
|
|
16
|
+
return;
|
|
17
|
+
const { data } = await supabase.from("products").select("id, unit, sale_price");
|
|
18
|
+
if (cancelled)
|
|
19
|
+
return;
|
|
20
|
+
const s = new Set();
|
|
21
|
+
const b = { empty: 0, zero: 0, unit: 0 };
|
|
22
|
+
for (const p of (data ?? [])) {
|
|
23
|
+
const isPiece = (p.unit ?? "").trim().toLowerCase() === "stuks";
|
|
24
|
+
if (!isPiece) {
|
|
25
|
+
s.add(p.id);
|
|
26
|
+
b.unit++;
|
|
27
|
+
}
|
|
28
|
+
else if (p.sale_price == null) {
|
|
29
|
+
s.add(p.id);
|
|
30
|
+
b.empty++;
|
|
31
|
+
}
|
|
32
|
+
else if (!(Number(p.sale_price) > 0)) {
|
|
33
|
+
s.add(p.id);
|
|
34
|
+
b.zero++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
setIds((cur) => (cur.size === s.size && [...s].every((x) => cur.has(x)) ? cur : s));
|
|
38
|
+
setBreakdown((cur) => cur.empty === b.empty && cur.zero === b.zero && cur.unit === b.unit ? cur : b);
|
|
39
|
+
};
|
|
40
|
+
void load();
|
|
41
|
+
const bundler = createBundler(() => void load(), 5000);
|
|
42
|
+
const channel = supabase
|
|
43
|
+
.channel(`voorraad-noprice-${Math.random().toString(36).slice(2)}`)
|
|
44
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "products" }, () => bundler.trigger())
|
|
45
|
+
.subscribe();
|
|
46
|
+
const onVis = () => bundler.flushIfPending();
|
|
47
|
+
document.addEventListener("visibilitychange", onVis);
|
|
48
|
+
return () => {
|
|
49
|
+
cancelled = true;
|
|
50
|
+
bundler.cancel();
|
|
51
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
52
|
+
supabase.removeChannel(channel);
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
55
|
+
return { noPriceIds: ids, noPriceBreakdown: breakdown };
|
|
56
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStockPushJobs.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockPushJobs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useStockPushJobs.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockPushJobs.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,qEAAqE;IACrE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAwBD,wBAAgB,gBAAgB,CAAC,KAAK,SAAK,yEAuD1C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB;aACT,OAAO;aAAW,MAAM,GAAG,IAAI;UAsB3D"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* inclusief de artikelnaam uit de Floriday-cache. Bedoeld om een testpush te
|
|
4
4
|
* kunnen volgen zonder databasekennis.
|
|
5
5
|
*/
|
|
6
|
+
import { MIN_POLL_MS } from "../lib/request-guard.js";
|
|
6
7
|
import { useQuery } from "@tanstack/react-query";
|
|
7
8
|
import { supabase } from "../integrations/supabase/client.js";
|
|
8
9
|
/** Floriday levert namen soms als taalobject ({ nl: "…", en: "…" }). */
|
|
@@ -28,7 +29,9 @@ function nameFromCache(data) {
|
|
|
28
29
|
export function useStockPushJobs(limit = 10) {
|
|
29
30
|
return useQuery({
|
|
30
31
|
queryKey: ["stock-push-jobs", limit],
|
|
31
|
-
refetchInterval:
|
|
32
|
+
refetchInterval: MIN_POLL_MS,
|
|
33
|
+
refetchIntervalInBackground: false,
|
|
34
|
+
refetchOnWindowFocus: false,
|
|
32
35
|
queryFn: async () => {
|
|
33
36
|
const { data, error } = await supabase
|
|
34
37
|
.from("floriday_continuous_stock_queue")
|
|
@@ -82,6 +85,8 @@ export function useCentralAuthBlockade() {
|
|
|
82
85
|
return useQuery({
|
|
83
86
|
queryKey: ["central-auth-blockade"],
|
|
84
87
|
refetchInterval: 60000,
|
|
88
|
+
refetchIntervalInBackground: false,
|
|
89
|
+
refetchOnWindowFocus: false,
|
|
85
90
|
queryFn: async () => {
|
|
86
91
|
const since = new Date(Date.now() - 30 * 60000).toISOString();
|
|
87
92
|
const { data, error } = await supabase
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStockSyncHealth.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockSyncHealth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useStockSyncHealth.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockSyncHealth.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,kDAAkD;IAClD,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAS3E;AAED,wBAAgB,kBAAkB,2EAyGjC"}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gezondheid van de voorraad-push-wachtrij richting Floriday.
|
|
3
|
-
*
|
|
4
|
-
* Bewust een venster van 24 uur: oude, allang achterhaalde fouten mogen de
|
|
5
|
-
* gebruiker niet eindeloos blijven waarschuwen. Regels die door een nieuwere
|
|
6
|
-
* voorraadstand zijn vervangen (`superseded`) tellen niet mee.
|
|
7
|
-
* Toont ook de meest voorkomende reden, het aantal artikelen en hoeveel
|
|
8
|
-
* daarvan geen prijs in het weekaanbod hebben.
|
|
9
|
-
*/
|
|
10
1
|
import { useQuery } from "@tanstack/react-query";
|
|
11
2
|
import { supabase } from "../integrations/supabase/client.js";
|
|
12
3
|
const STALE_MINUTES = 10;
|
|
@@ -29,6 +20,8 @@ export function useStockSyncHealth() {
|
|
|
29
20
|
return useQuery({
|
|
30
21
|
queryKey: ["stock-sync-health"],
|
|
31
22
|
refetchInterval: 60000,
|
|
23
|
+
refetchIntervalInBackground: false,
|
|
24
|
+
refetchOnWindowFocus: false,
|
|
32
25
|
queryFn: async () => {
|
|
33
26
|
const since = new Date(Date.now() - WINDOW_HOURS * 3600000).toISOString();
|
|
34
27
|
const { data, error } = await supabase
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVoorraadData.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoorraadData.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useVoorraadData.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoorraadData.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAsB5E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;;EA2U1D"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createBundler, pageRequestGuard } from "../lib/request-guard.js";
|
|
2
3
|
import { supabase } from "../integrations/supabase/client.js";
|
|
3
4
|
import { normaliseTradeItem } from "../lib/floriday-client.js";
|
|
4
5
|
import { floridayInfoFromCache } from "../lib/floriday-info.js";
|
|
@@ -45,6 +46,8 @@ export function useVoorraadData(connectionId) {
|
|
|
45
46
|
const [error, setError] = useState(null);
|
|
46
47
|
const { items: localItems, loading: localLoading, reload: reloadLocal, } = useLocalTradeItems(connectionId);
|
|
47
48
|
const loadPublished = useCallback(async () => {
|
|
49
|
+
if (!pageRequestGuard.allow("products-overzicht"))
|
|
50
|
+
return;
|
|
48
51
|
setLoading(true);
|
|
49
52
|
setError(null);
|
|
50
53
|
try {
|
|
@@ -148,30 +151,46 @@ export function useVoorraadData(connectionId) {
|
|
|
148
151
|
useEffect(() => {
|
|
149
152
|
loadPublished();
|
|
150
153
|
}, [loadPublished]);
|
|
154
|
+
// Realtime-seintjes gebundeld: hooguit één herlaadactie per 2 s, niet in een
|
|
155
|
+
// verborgen tabblad (wel bij terugkeren). Voorkomt opvraag-stormen (1.0.111).
|
|
156
|
+
const reloadRef = useRef({ stock: false, all: false });
|
|
151
157
|
useEffect(() => {
|
|
152
|
-
const
|
|
158
|
+
const bundler = createBundler(() => {
|
|
159
|
+
const r = reloadRef.current;
|
|
160
|
+
reloadRef.current = { stock: false, all: false };
|
|
161
|
+
if (r.all) {
|
|
162
|
+
loadPublished();
|
|
163
|
+
reloadLocal();
|
|
164
|
+
}
|
|
165
|
+
else if (r.stock && pageRequestGuard.allow("trade_item_stock")) {
|
|
166
|
+
fetchStockMap(connectionId).then(setStockMap).catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
}, 2000);
|
|
169
|
+
const markAll = () => {
|
|
170
|
+
reloadRef.current.all = true;
|
|
171
|
+
bundler.trigger();
|
|
172
|
+
};
|
|
173
|
+
const markStock = () => {
|
|
174
|
+
reloadRef.current.stock = true;
|
|
175
|
+
bundler.trigger();
|
|
176
|
+
};
|
|
177
|
+
let channel = supabase
|
|
153
178
|
.channel(`voorraad-stock-${connectionId ?? "eigen"}`)
|
|
154
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "trade_item_stock" },
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "product_bom_items" }, () => {
|
|
168
|
-
loadPublished();
|
|
169
|
-
})
|
|
170
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "product_group_items" }, () => {
|
|
171
|
-
loadPublished();
|
|
172
|
-
})
|
|
173
|
-
.subscribe();
|
|
179
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "trade_item_stock" }, markStock)
|
|
180
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "products" }, markAll);
|
|
181
|
+
if (connectionId) {
|
|
182
|
+
channel = channel.on("postgres_changes", { event: "*", schema: "public", table: "floriday_trade_items_cache", filter: `connection_id=eq.${connectionId}` }, markAll);
|
|
183
|
+
}
|
|
184
|
+
channel = channel
|
|
185
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "product_groups" }, markAll)
|
|
186
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "product_bom_items" }, markAll)
|
|
187
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "product_group_items" }, markAll);
|
|
188
|
+
channel.subscribe();
|
|
189
|
+
const onVis = () => bundler.flushIfPending();
|
|
190
|
+
document.addEventListener("visibilitychange", onVis);
|
|
174
191
|
return () => {
|
|
192
|
+
bundler.cancel();
|
|
193
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
175
194
|
supabase.removeChannel(channel);
|
|
176
195
|
};
|
|
177
196
|
}, [connectionId, loadPublished, reloadLocal]);
|
|
@@ -274,7 +293,8 @@ export function useVoorraadData(connectionId) {
|
|
|
274
293
|
return i;
|
|
275
294
|
return { ...i, floriday: info };
|
|
276
295
|
};
|
|
277
|
-
|
|
296
|
+
// Stabiele identiteit: alleen een nieuwe lijst als de onderliggende data verandert.
|
|
297
|
+
const merged = useMemo(() => [
|
|
278
298
|
...localItems.map((i) => applyProductGroupReservation(classify(attachFloriday(applyStock(i, lookupStock(stockMap, {
|
|
279
299
|
tradeItemKey: i.tradeItemKey ?? null,
|
|
280
300
|
floridayId: i.floridayId ?? null,
|
|
@@ -287,7 +307,8 @@ export function useVoorraadData(connectionId) {
|
|
|
287
307
|
tradeItemKey: i.tradeItemKey ?? null,
|
|
288
308
|
productId: i.productId ?? null,
|
|
289
309
|
})))))),
|
|
290
|
-
|
|
310
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
311
|
+
], [localItems, publishedItems, stockMap, typeSets, floridayInfoMap, floridayStockMap]);
|
|
291
312
|
return {
|
|
292
313
|
items: merged,
|
|
293
314
|
loading: loading || localLoading,
|