@flowselections/floriday-voorraad 1.0.103 → 1.0.108

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 (42) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.js +3 -2
  3. package/dist-lib/components/voorraad/FloridayLoadCarriers.d.ts +9 -0
  4. package/dist-lib/components/voorraad/FloridayLoadCarriers.d.ts.map +1 -0
  5. package/dist-lib/components/voorraad/FloridayLoadCarriers.js +60 -0
  6. package/dist-lib/components/voorraad/FustenPage.d.ts.map +1 -1
  7. package/dist-lib/components/voorraad/FustenPage.js +14 -6
  8. package/dist-lib/components/voorraad/NieuwArtikelPage.d.ts.map +1 -1
  9. package/dist-lib/components/voorraad/NieuwArtikelPage.js +10 -6
  10. package/dist-lib/components/voorraad/StockBulkButtons.d.ts.map +1 -1
  11. package/dist-lib/components/voorraad/StockBulkButtons.js +3 -6
  12. package/dist-lib/components/voorraad/StockSyncStatus.d.ts.map +1 -1
  13. package/dist-lib/components/voorraad/StockSyncStatus.js +10 -2
  14. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
  15. package/dist-lib/components/voorraad/VoorraadStockCard.js +6 -4
  16. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  17. package/dist-lib/components/voorraad/VoorraadTable.js +6 -1
  18. package/dist-lib/hooks/useFloridayWriteMode.d.ts +7 -0
  19. package/dist-lib/hooks/useFloridayWriteMode.d.ts.map +1 -0
  20. package/dist-lib/hooks/useFloridayWriteMode.js +21 -0
  21. package/dist-lib/hooks/useStockSyncHealth.d.ts +9 -0
  22. package/dist-lib/hooks/useStockSyncHealth.d.ts.map +1 -1
  23. package/dist-lib/hooks/useStockSyncHealth.js +23 -0
  24. package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
  25. package/dist-lib/hooks/useTradeItemStock.js +23 -10
  26. package/dist-lib/integrations/supabase/types.d.ts +2247 -131
  27. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  28. package/dist-lib/lib/__tests__/floriday-write-mode.test.d.ts +2 -0
  29. package/dist-lib/lib/__tests__/floriday-write-mode.test.d.ts.map +1 -0
  30. package/dist-lib/lib/__tests__/floriday-write-mode.test.js +40 -0
  31. package/dist-lib/lib/floriday-write-mode.d.ts +21 -0
  32. package/dist-lib/lib/floriday-write-mode.d.ts.map +1 -0
  33. package/dist-lib/lib/floriday-write-mode.js +40 -0
  34. package/dist-lib/lib/fusten.d.ts +20 -0
  35. package/dist-lib/lib/fusten.d.ts.map +1 -1
  36. package/dist-lib/lib/fusten.js +48 -41
  37. package/dist-lib/lib/stock-scope.d.ts +3 -1
  38. package/dist-lib/lib/stock-scope.d.ts.map +1 -1
  39. package/dist-lib/lib/stock-scope.js +3 -1
  40. package/package.json +1 -1
  41. package/sql/AGENTS.md +2 -8
  42. package/sql/floriday-voorraad-schema.sql +194 -71
@@ -1 +1 @@
1
- {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA8MA,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,GACZ,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;CACjB,+BA0tEL"}
1
+ {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AA+MA,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EACX,SAAS,EACT,WAAW,GACZ,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;CACjB,+BA0tEL"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { saveOwnPackings, packingRowsFromWizard } from "../../lib/product-packings.js";
3
+ import { FloridayLoadCarriers } from "./FloridayLoadCarriers.js";
3
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
5
  import { Link, useNavigate } from "@tanstack/react-router";
5
6
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button, Card, CardContent, Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, toast, } from "@flowselections/core";
@@ -2526,7 +2527,7 @@ function StepWarehouse({ data, update, activeId }) {
2526
2527
  * 3. Reguliere of klantspecifieke belading (multi-select organisaties)
2527
2528
  * 4. Optionele artikelfoto in dit fust (upload via Floriday /media)
2528
2529
  */
2529
- function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCustomers, activeId, packingStatus, }) {
2530
+ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCustomers, activeId, packingStatus, parentFloridayId, }) {
2530
2531
  const [dialogOpen, setDialogOpen] = useState(false);
2531
2532
  const [editingId, setEditingId] = useState(null);
2532
2533
  const openNew = () => {
@@ -2576,7 +2577,7 @@ function StepBeladingen({ data, update, packagingTypes, loadCarriers, floridayCu
2576
2577
  return (_jsxs("tr", { className: "hover:bg-muted/30", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "radio", name: "primary-packing", checked: Boolean(p.isPrimary), onChange: () => setPrimary(p.id), "aria-label": "Als primaire belading instellen" }) }), _jsxs("td", { className: "px-3 py-2", children: [_jsx("div", { className: "font-medium", children: pkgLabel
2577
2578
  ? `${codeOf(pkgLabel)}: ${labelOf(pkgLabel)}`
2578
2579
  : p.packagingType || "—" }), p.source === "SUPPLIER" && (_jsx("div", { className: "text-xs text-muted-foreground", children: p.packagingSupplierName ?? p.packagingSupplierId }))] }), _jsx("td", { className: "px-3 py-2", children: LOAD_CARRIER_LABELS[p.loadCarrier] ?? p.loadCarrier ?? "—" }), _jsxs("td", { className: "px-3 py-2 tabular-nums", children: [p.layersPerLoadCarrier, "\u00D7", p.packagesPerLayer, "\u00D7", p.unitsPerPackage] }), _jsx("td", { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [p.isCustomerSpecific ? (_jsxs("span", { className: "rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-900", children: ["Klantspecifiek (", p.customerOrganizationIds.length, ")"] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "Regulier" })), p.isHiddenForDirectSales && (_jsx("span", { className: "rounded bg-slate-200 px-2 py-0.5 text-xs text-slate-700", children: "Verborgen" }))] }) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => openEdit(p.id), children: "Bewerk" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(p.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })] }, p.id));
2579
- }) })] }) })), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: openNew, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Belading toevoegen"] }), _jsx(PackingConfigDialog, { open: dialogOpen, onOpenChange: (o) => {
2580
+ }) })] }) })), _jsx(FloridayLoadCarriers, { connectionId: activeId, floridayId: parentFloridayId }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: openNew, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Belading toevoegen"] }), _jsx(PackingConfigDialog, { open: dialogOpen, onOpenChange: (o) => {
2580
2581
  setDialogOpen(o);
2581
2582
  if (!o)
2582
2583
  setEditingId(null);
@@ -0,0 +1,9 @@
1
+ export declare function loadCarriersFromCache(data: unknown): Array<{
2
+ loadCarrier: string | null;
3
+ primary: boolean;
4
+ }>;
5
+ export declare function FloridayLoadCarriers({ connectionId, floridayId, }: {
6
+ connectionId: string | null;
7
+ floridayId: string | null;
8
+ }): import("react").JSX.Element;
9
+ //# sourceMappingURL=FloridayLoadCarriers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FloridayLoadCarriers.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FloridayLoadCarriers.tsx"],"names":[],"mappings":"AASA,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,KAAK,CAAC;IAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAO5G;AAED,wBAAgB,oBAAoB,CAAC,EACnC,YAAY,EACZ,UAAU,GACX,EAAE;IACD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,+BAsDA"}
@@ -0,0 +1,60 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Ladingdrager per verpakking zoals Floriday die kent:
4
+ * floriday_trade_items_cache.data.packingConfigurations[].loadCarrier,
5
+ * gezocht op connection_id + floriday_id (nooit op naam). Alleen lezen.
6
+ */
7
+ import { useEffect, useState } from "react";
8
+ import { supabase } from "../../integrations/supabase/client.js";
9
+ import { LOAD_CARRIER_LABELS } from "../../lib/floriday-info.js";
10
+ export function loadCarriersFromCache(data) {
11
+ const list = data?.packingConfigurations;
12
+ if (!Array.isArray(list))
13
+ return [];
14
+ return list.map((p) => ({
15
+ loadCarrier: typeof p?.loadCarrier === "string" && p.loadCarrier.trim() ? p.loadCarrier : null,
16
+ primary: p?.primary === true || p?.isPrimary === true,
17
+ }));
18
+ }
19
+ export function FloridayLoadCarriers({ connectionId, floridayId, }) {
20
+ const [state, setState] = useState({ kind: "loading" });
21
+ useEffect(() => {
22
+ if (!connectionId || !floridayId)
23
+ return;
24
+ let cancelled = false;
25
+ setState({ kind: "loading" });
26
+ void (async () => {
27
+ const { data, error } = await supabase
28
+ .from("floriday_trade_items_cache")
29
+ .select("data")
30
+ .eq("connection_id", connectionId)
31
+ .eq("floriday_id", floridayId)
32
+ .maybeSingle();
33
+ if (cancelled)
34
+ return;
35
+ if (error)
36
+ setState({ kind: "error", message: error.message });
37
+ else if (!data)
38
+ setState({ kind: "missing" });
39
+ else
40
+ setState({ kind: "ok", rows: loadCarriersFromCache(data.data) });
41
+ })();
42
+ return () => {
43
+ cancelled = true;
44
+ };
45
+ }, [connectionId, floridayId]);
46
+ let body;
47
+ if (!connectionId || !floridayId)
48
+ body = "Niet gekoppeld aan Floriday";
49
+ else if (state.kind === "loading")
50
+ body = "Laden…";
51
+ else if (state.kind === "error")
52
+ body = `Niet leesbaar: ${state.message}`;
53
+ else if (state.kind === "missing")
54
+ body = "Artikel niet gevonden in de Floriday-gegevens";
55
+ else if (state.rows.length === 0)
56
+ body = "Floriday heeft geen belading voor dit artikel";
57
+ else
58
+ body = (_jsx("ul", { className: "list-disc pl-5", children: state.rows.map((r, i) => (_jsxs("li", { children: ["Belading ", i + 1, r.primary ? " (primair)" : "", ":", " ", r.loadCarrier ? (LOAD_CARRIER_LABELS[r.loadCarrier] ?? r.loadCarrier) : "—"] }, i))) }));
59
+ return (_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-sm", children: [_jsx("div", { className: "mb-1 font-medium", children: "Ladingdrager volgens Floriday" }), _jsx("div", { className: "text-muted-foreground", children: body })] }));
60
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"FustenPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FustenPage.tsx"],"names":[],"mappings":"AAkKA,wBAAgB,UAAU,gCAgPzB"}
1
+ {"version":3,"file":"FustenPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/FustenPage.tsx"],"names":[],"mappings":"AA4KA,wBAAgB,UAAU,gCA6PzB"}
@@ -5,29 +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 { fetchFustMutations, formatFustDimensions, } from "../../lib/fusten.js";
8
+ import { fetchFloricodeFustMap, fetchFustMutations, formatFustDimensions, resolveFustDimensions, } from "../../lib/fusten.js";
9
9
  /** Afmetingen tonen en handmatig aanvullen wanneer de catalogus ze niet kent. */
10
- function DimensionCell({ fust, onSave, }) {
10
+ function DimensionCell({ fust, floricode, 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 label = formatFustDimensions(fust);
17
+ const resolved = resolveFustDimensions(fust, floricode);
18
+ const label = formatFustDimensions(resolved);
18
19
  const toNum = (v) => {
19
20
  const n = Math.trunc(Number(v));
20
21
  return Number.isFinite(n) && n > 0 ? n : null;
21
22
  };
22
23
  if (!editing) {
23
- return (_jsxs("div", { children: [_jsx("div", { className: label ? "tabular-nums" : "text-muted-foreground", children: label ?? "Onbekend" }), _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" : "handmatig" })), _jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => {
24
25
  setDraft({
25
26
  length_mm: fust.length_mm?.toString() ?? "",
26
27
  width_mm: fust.width_mm?.toString() ?? "",
27
28
  height_mm: fust.height_mm?.toString() ?? "",
28
29
  });
29
30
  setEditing(true);
30
- }, children: label ? "Wijzigen" : "Afmeting toevoegen" })] }));
31
+ }, children: resolved.source === "handmatig" ? "Wijzigen" : resolved.source === "floricode" ? "Handmatig aanvullen" : "Afmeting toevoegen" })] }));
31
32
  }
32
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 () => {
33
34
  await onSave({
@@ -66,6 +67,13 @@ export function FustenPage() {
66
67
  const { activeId } = useFloridayConnection();
67
68
  const { items, loading, syncing, error, sync, adjust, patch } = useFusten(activeId);
68
69
  const [search, setSearch] = useState("");
70
+ const [floricode, setFloricode] = useState(null);
71
+ const [floricodeError, setFloricodeError] = useState(null);
72
+ useEffect(() => {
73
+ fetchFloricodeFustMap()
74
+ .then((m) => { setFloricode(m); setFloricodeError(null); })
75
+ .catch((e) => setFloricodeError(e.message));
76
+ }, []);
69
77
  const [busyId, setBusyId] = useState(null);
70
78
  const [amounts, setAmounts] = useState({});
71
79
  const [negative, setNegative] = useState([]);
@@ -107,7 +115,7 @@ export function FustenPage() {
107
115
  void loadNegative();
108
116
  }
109
117
  };
110
- 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] })), 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, 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
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
111
119
  ? "text-rose-600"
112
120
  : ""}`, 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) => {
113
121
  const v = Math.max(0, Math.trunc(Number(e.target.value) || 0));
@@ -1 +1 @@
1
- {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,gBAAgB,gCA6N/B"}
1
+ {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,gBAAgB,gCAkO/B"}
@@ -13,15 +13,16 @@ import { supabase } from "../../integrations/supabase/client.js";
13
13
  import { validateDraftForPublish } from "../../lib/floriday-import-validator.js";
14
14
  import { ArtikelWizard } from "./ArtikelWizard.js";
15
15
  import { useFloridayConnection } from "../../hooks/useFloridayConnection.js";
16
- import { STOCK_WRITE_NEEDS_CONNECTION } from "../../lib/stock-scope.js";
16
+ import { LOCAL_STOCK_TEXT } from "../../lib/stock-scope.js";
17
17
  import { FloridayConnectionNotice } from "./FloridayConnectionNotice.js";
18
18
  const STEPS = ["Naam en code", "Voorraad", "Prijs"];
19
19
  export function NieuwArtikelPage() {
20
20
  const navigate = useNavigate();
21
21
  const conn = useFloridayConnection();
22
- // Zonder actieve koppeling kan set_product_stock (nog) niet opslaan: het
23
- // voorraadveld staat dan uit en er wordt geen voorraad verstuurd.
24
- const canSaveStock = conn.status === "actief";
22
+ // Sinds 1.0.107 slaat set_product_stock zonder koppeling op in de lokale
23
+ // voorraadcontext; alleen tijdens het controleren van de koppeling staat het veld uit.
24
+ const canSaveStock = conn.status !== "laden";
25
+ const hasFloriday = conn.status === "actief";
25
26
  const [full, setFull] = useState(false);
26
27
  const [step, setStep] = useState(0);
27
28
  const [name, setName] = useState("");
@@ -70,6 +71,9 @@ export function NieuwArtikelPage() {
70
71
  ? await supabase.rpc("set_product_stock", {
71
72
  p_product_id: id,
72
73
  p_physical_stock: Math.trunc(Number(stock) || 0),
74
+ // Nieuw artikel: minimum en inkomend bewust 0.
75
+ p_minimum_stock: 0,
76
+ p_incoming_stock: 0,
73
77
  p_notes: "Nieuw artikel",
74
78
  p_change_source: "NEW_ARTICLE",
75
79
  })
@@ -93,7 +97,7 @@ export function NieuwArtikelPage() {
93
97
  if (createdId) {
94
98
  const missing = validateDraftForPublish({}).missing;
95
99
  const hasPrice = Number(price.replace(",", ".")) > 0;
96
- return (_jsx("main", { className: "p-6 space-y-4 max-w-2xl", children: _jsxs(Card, { className: "p-5 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-emerald-700", children: [_jsx(Check, { className: "h-5 w-5" }), _jsxs("p", { className: "font-medium", children: [name, " is aangemaakt."] })] }), canSaveStock ? (_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)))] })] })) : (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Er is geen voorraad opgeslagen. ", STOCK_WRITE_NEEDS_CONNECTION] })), _jsxs("div", { className: "flex flex-wrap gap-2", children: [canSaveStock && (_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" }) })] })] }) }));
100
+ return (_jsx("main", { className: "p-6 space-y-4 max-w-2xl", children: _jsxs(Card, { className: "p-5 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-emerald-700", children: [_jsx(Check, { className: "h-5 w-5" }), _jsxs("p", { className: "font-medium", children: [name, " is aangemaakt."] })] }), 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" }) })] })] }) }));
97
101
  }
98
- 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: conn.status === "laden" ? "Koppeling wordt gecontroleerd…" : STOCK_WRITE_NEEDS_CONNECTION }))] })), 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\")." })] })), 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(), 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"] }))] })] }), canSaveStock && (_jsx("button", { type: "button", className: "text-sm text-muted-foreground underline hover:text-foreground", onClick: () => setFull(true), children: "Meteen alle Floriday-gegevens invullen" }))] }));
102
+ 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\")." })] })), 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(), 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" }))] }));
99
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"StockBulkButtons.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockBulkButtons.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAgH3D,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,+BAorBA"}
1
+ {"version":3,"file":"StockBulkButtons.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockBulkButtons.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAgH3D,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,+BAgrBA"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { STOCK_WRITE_NEEDS_CONNECTION } from "../../lib/stock-scope.js";
2
+ import { LOCAL_STOCK_TEXT } from "../../lib/stock-scope.js";
3
3
  import { positiveIntOrNull, saveOwnPackings } from "../../lib/product-packings.js";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import * as XLSX from "xlsx";
@@ -202,10 +202,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
202
202
  };
203
203
  /** Stap 1: bestand lezen en per rij controleren (nog niets opgeslagen). */
204
204
  const handleFile = async (file) => {
205
- if (!activeConnectionId) {
206
- toast.error("Geen actieve Floriday-koppeling. Kijk bij Instellingen › Floriday.");
207
- return;
208
- }
205
+ // Zonder koppeling importeert de database in de lokale voorraadcontext (1.0.107).
209
206
  setBusy("lezen");
210
207
  setBanner(null);
211
208
  setPrepared(null);
@@ -578,7 +575,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
578
575
  : banner?.kind === "success"
579
576
  ? "border-primary/40 bg-primary/5"
580
577
  : "bg-muted/30";
581
- return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: downloadTemplate, disabled: items.length === 0, children: [_jsx(FileDown, { className: "mr-2 h-4 w-4" }), "Template downloaden"] }), _jsxs(Button, { variant: "outline", onClick: () => setOpen(true), disabled: !activeConnectionId, title: activeConnectionId ? undefined : STOCK_WRITE_NEEDS_CONNECTION, children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), "Voorraad importeren"] }), !activeConnectionId && (_jsx("span", { className: "text-xs text-muted-foreground", children: STOCK_WRITE_NEEDS_CONNECTION })), _jsx(Dialog, { open: open, onOpenChange: (o) => !busy && setOpen(o), children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Voorraad importeren" }), _jsxs(DialogDescription, { children: ["Upload een Excel-bestand (op basis van de template). Bestaande artikelen worden gematcht op", " ", _jsx("strong", { children: "Artikelcode" }), "; liggende, minimum- en inkomende voorraad worden bijgewerkt. Gereserveerde voorraad wordt nooit ge\u00EFmporteerd. Nieuwe artikelcodes worden als concept aangemaakt en niet naar Floriday gepubliceerd."] })] }), _jsxs("div", { className: "space-y-4", children: [_jsx("input", { ref: fileRef, type: "file", accept: ".xlsx,.xls", className: "block w-full text-sm file:mr-3 file:rounded-md file:border-0 file:bg-primary file:px-3 file:py-2 file:text-primary-foreground hover:file:bg-primary/90 disabled:opacity-50", disabled: !!busy, onChange: (e) => {
578
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: downloadTemplate, disabled: items.length === 0, children: [_jsx(FileDown, { className: "mr-2 h-4 w-4" }), "Template downloaden"] }), _jsxs(Button, { variant: "outline", onClick: () => setOpen(true), title: activeConnectionId ? undefined : LOCAL_STOCK_TEXT, children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), "Voorraad importeren"] }), !activeConnectionId && (_jsx("span", { className: "text-xs text-muted-foreground", children: LOCAL_STOCK_TEXT })), _jsx(Dialog, { open: open, onOpenChange: (o) => !busy && setOpen(o), children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Voorraad importeren" }), _jsxs(DialogDescription, { children: ["Upload een Excel-bestand (op basis van de template). Bestaande artikelen worden gematcht op", " ", _jsx("strong", { children: "Artikelcode" }), "; liggende, minimum- en inkomende voorraad worden bijgewerkt. Gereserveerde voorraad wordt nooit ge\u00EFmporteerd. Nieuwe artikelcodes worden als concept aangemaakt en niet naar Floriday gepubliceerd."] })] }), _jsxs("div", { className: "space-y-4", children: [_jsx("input", { ref: fileRef, type: "file", accept: ".xlsx,.xls", className: "block w-full text-sm file:mr-3 file:rounded-md file:border-0 file:bg-primary file:px-3 file:py-2 file:text-primary-foreground hover:file:bg-primary/90 disabled:opacity-50", disabled: !!busy, onChange: (e) => {
582
579
  const f = e.target.files?.[0];
583
580
  if (f)
584
581
  void handleFile(f);
@@ -1 +1 @@
1
- {"version":3,"file":"StockSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockSyncStatus.tsx"],"names":[],"mappings":"AAYA,wBAAgB,eAAe,uCA6E9B"}
1
+ {"version":3,"file":"StockSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockSyncStatus.tsx"],"names":[],"mappings":"AAeA,wBAAgB,eAAe,uCAwH9B"}
@@ -10,6 +10,9 @@ import { useServerFn } from "@tanstack/react-start";
10
10
  import { toast } from "sonner";
11
11
  import { useStockSyncHealth } from "../../hooks/useStockSyncHealth.js";
12
12
  import { retryFailedStockSync } from "../../lib/stock-sync.functions.js";
13
+ import { Link } from "@tanstack/react-router";
14
+ import { useFloridayWriteMode } from "../../hooks/useFloridayWriteMode.js";
15
+ import { gateBlockedText, waitCauseText } from "../../lib/floriday-write-mode.js";
13
16
  export function StockSyncStatus() {
14
17
  const { data } = useStockSyncHealth();
15
18
  const queryClient = useQueryClient();
@@ -24,11 +27,16 @@ export function StockSyncStatus() {
24
27
  },
25
28
  onError: (e) => toast.error(e?.message ?? "Opnieuw proberen is niet gelukt."),
26
29
  });
30
+ const showCause = Boolean(data && (data.stalePending > 0 || data.blocked.length > 0 || data.blockedWithoutProduct > 0));
31
+ const writeMode = useFloridayWriteMode(showCause);
27
32
  if (!data)
28
33
  return null;
29
34
  const { failed, stalePending, oldestPendingAt, topReason, articleCount, noPriceCount } = data;
30
- if (failed === 0 && stalePending === 0)
35
+ const blocked = data.blocked;
36
+ const blockedCount = blocked.length + data.blockedWithoutProduct;
37
+ if (failed === 0 && stalePending === 0 && blockedCount === 0)
31
38
  return null;
39
+ const cause = showCause ? waitCauseText(writeMode) : null;
32
40
  const parts = [];
33
41
  if (failed > 0) {
34
42
  parts.push(`${failed} voorraadwijziging${failed === 1 ? "" : "en"} mislukt`);
@@ -36,5 +44,5 @@ export function StockSyncStatus() {
36
44
  if (stalePending > 0) {
37
45
  parts.push(`${stalePending} wacht${stalePending === 1 ? "" : "en"} langer dan 10 minuten`);
38
46
  }
39
- return (_jsxs("div", { className: "flex 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 space-y-0.5", children: [_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: "Voorraadsync naar Floriday: " }), parts.join(" · "), " (laatste 24 uur), voor ", articleCount, " artikel", articleCount === 1 ? "" : "en", ".", oldestPendingAt && (_jsxs("span", { className: "opacity-80", children: [" ", "Oudste wachtende regel: ", new Date(oldestPendingAt).toLocaleString("nl-NL"), "."] }))] }), topReason && (_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: "Meest voorkomende reden: " }), topReason, "."] })), noPriceCount > 0 && (_jsxs("div", { className: "opacity-90", children: [noPriceCount, " van deze artikelen ", noPriceCount === 1 ? "heeft" : "hebben", " nog geen prijs in het weekaanbod; het aantal wordt dan zonder prijs doorgezet."] }))] }), failed > 0 && (_jsxs("button", { type: "button", className: "inline-flex shrink-0 items-center rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-1 text-xs font-medium text-amber-900 hover:bg-amber-500/20 disabled:opacity-50", disabled: mutation.isPending, onClick: () => mutation.mutate(), children: [_jsx(RotateCw, { className: `mr-1 h-3.5 w-3.5 ${mutation.isPending ? "animate-spin" : ""}` }), "Opnieuw proberen"] }))] }));
47
+ return (_jsxs("div", { className: "flex 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 space-y-0.5", children: [parts.length > 0 && (_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: "Voorraadsync naar Floriday: " }), parts.join(" · "), " (laatste 24 uur), voor ", articleCount, " artikel", articleCount === 1 ? "" : "en", ".", oldestPendingAt && (_jsxs("span", { className: "opacity-80", children: [" ", "Oudste wachtende regel: ", new Date(oldestPendingAt).toLocaleString("nl-NL"), "."] }))] })), cause && (_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: "Oorzaak: " }), cause] })), blockedCount > 0 && (_jsxs("div", { className: "space-y-0.5", children: [_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: "Wacht op Floriday \u2013 verpakkingsverschil: " }), blockedCount, " regel", blockedCount === 1 ? "" : "s", " tegengehouden."] }), _jsxs("ul", { className: "list-disc pl-5 text-xs", children: [blocked.map((b) => (_jsx("li", { children: _jsx(Link, { to: "/voorraad/$id", params: { id: b.productId }, className: "underline hover:no-underline", children: gateBlockedText({ name: b.name, code: b.code }) }) }, b.productId))), data.blockedWithoutProduct > 0 && (_jsxs("li", { children: [data.blockedWithoutProduct, " regel", data.blockedWithoutProduct === 1 ? "" : "s", " zonder bekend artikel"] }))] })] })), topReason && (_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: "Meest voorkomende reden: " }), topReason, "."] })), noPriceCount > 0 && (_jsxs("div", { className: "opacity-90", children: [noPriceCount, " van deze artikelen ", noPriceCount === 1 ? "heeft" : "hebben", " nog geen prijs in het weekaanbod; het aantal wordt dan zonder prijs doorgezet."] }))] }), failed > 0 && (_jsxs("button", { type: "button", className: "inline-flex shrink-0 items-center rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-1 text-xs font-medium text-amber-900 hover:bg-amber-500/20 disabled:opacity-50", disabled: mutation.isPending, onClick: () => mutation.mutate(), children: [_jsx(RotateCw, { className: `mr-1 h-3.5 w-3.5 ${mutation.isPending ? "animate-spin" : ""}` }), "Opnieuw proberen"] }))] }));
40
48
  }
@@ -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,sCA0MzF"}
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"}
@@ -5,7 +5,7 @@ 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
7
  import { FloridaySentCard } from "./FloridaySentCard.js";
8
- import { STOCK_WRITE_NEEDS_CONNECTION } from "../../lib/stock-scope.js";
8
+ import { LOCAL_STOCK_TEXT } from "../../lib/stock-scope.js";
9
9
  export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productId }) {
10
10
  const keys = stockKeysFor({
11
11
  tradeItemId,
@@ -89,8 +89,10 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
89
89
  return raw;
90
90
  }
91
91
  };
92
+ // Opslaan via set_product_stock werkt ook zonder koppeling (lokale context, 1.0.107).
93
+ const canWrite = !!productId || !!connectionId;
92
94
  const handleSave = async () => {
93
- if (!connectionId)
95
+ if (!canWrite)
94
96
  return;
95
97
  setSaving(true);
96
98
  try {
@@ -104,9 +106,9 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
104
106
  setSaving(false);
105
107
  }
106
108
  };
107
- 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: !connectionId, value: String(values[id] ?? 0), onChange: (e) => {
109
+ 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) => {
108
110
  setTouched(true);
109
111
  setValues((v) => ({ ...v, [id]: Number(e.target.value) || 0 }));
110
112
  } })] }));
111
- 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: !connectionId, 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: STOCK_WRITE_NEEDS_CONNECTION })), _jsxs(Button, { onClick: handleSave, disabled: saving || !connectionId, 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 })] }));
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 })] }));
112
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadTable.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AA8EzE,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,EACP,aAAa,EACb,QAAQ,EACR,cAAc,EACd,WAAW,EACX,cAAc,GACf,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,+BA4TA"}
1
+ {"version":3,"file":"VoorraadTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadTable.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAgFzE,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,EACP,aAAa,EACb,QAAQ,EACR,cAAc,EACd,WAAW,EACX,cAAc,GACf,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,+BAmUA"}
@@ -8,6 +8,8 @@ import { useVoorraadCustomFields } from "../../hooks/useVoorraadCustomFields.js"
8
8
  import { formatCustomValue } from "../../lib/voorraad-custom-fields.js";
9
9
  import { InventoryStatusBadge } from "./InventoryStatusBadge.js";
10
10
  import { loadIncompletePackingIds, PACKING_INCOMPLETE_TEXT } from "../../lib/product-packings.js";
11
+ import { useFloridayWriteMode } from "../../hooks/useFloridayWriteMode.js";
12
+ import { waitCauseText } from "../../lib/floriday-write-mode.js";
11
13
  import { PRODUCT_TYPE_CLASSES, PRODUCT_TYPE_HELP, PRODUCT_TYPE_LABELS, toStructureType } from "../../lib/product-types.js";
12
14
  const columns = [
13
15
  { key: "quantity", label: "Liggend" },
@@ -90,6 +92,9 @@ export function settingKeyOf(item) {
90
92
  }
91
93
  export function VoorraadTable({ items, loading, settingsByKey, selected, onToggleSelect, onToggleAll, onOpenSettings, }) {
92
94
  const navigate = useNavigate();
95
+ // Oorzaak naast "Wacht op Floriday" (alleen lezen, alleen als er zo'n melding is).
96
+ const anyWaiting = items.some((i) => Boolean(customValue(i, "floriday_sync")?.dirty));
97
+ const waitCause = waitCauseText(useFloridayWriteMode(anyWaiting));
93
98
  // Artikelen zonder volledige primaire verpakking (karindeling kan ze niet plannen).
94
99
  const [incompletePacking, setIncompletePacking] = useState(new Set());
95
100
  const productIdsKey = items.map((i) => i.productId ?? "").join(",");
@@ -155,7 +160,7 @@ export function VoorraadTable({ items, loading, settingsByKey, selected, onToggl
155
160
  : "Niet online in Floriday", title: floriday
156
161
  ? "Online in Floriday"
157
162
  : "Niet online in Floriday", className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2 py-0.5 text-[11px] font-medium leading-none text-muted-foreground", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full " +
158
- (floriday ? "bg-emerald-500" : "bg-red-500"), "aria-hidden": true }), _jsx("span", { children: "Floriday" })] })] }), Boolean(customValue(item, "floriday_sync")?.dirty) && (_jsx("div", { className: "mt-1 inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-medium leading-none text-amber-800", title: "Opgeslagen in dit systeem, wacht nog op doorgifte naar Floriday", children: "Wacht op Floriday" })), low && (_jsx("div", { className: "text-xs text-destructive", children: "onder minimumvoorraad" })), overReserved && (_jsx("div", { className: "text-xs text-amber-700", title: "Er is meer gereserveerd voor orders dan er ligt. Controleer open orders.", children: "gereserveerd groter dan liggend" }))] }), _jsx("td", { className: "p-3", onClick: (e) => e.stopPropagation(), children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(InventoryStatusBadge, { setting: setting }), settingProductId && onOpenSettings && (_jsx(Button, { variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => onOpenSettings(settingProductId), "aria-label": `Voorraadsturing van ${item.name}`, children: _jsx(Settings2, { className: "h-4 w-4" }) }))] }) }), showType && _jsx("td", { className: "p-3", children: (() => {
163
+ (floriday ? "bg-emerald-500" : "bg-red-500"), "aria-hidden": true }), _jsx("span", { children: "Floriday" })] })] }), Boolean(customValue(item, "floriday_sync")?.dirty) && (_jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-1.5", children: [_jsx("div", { className: "inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-medium leading-none text-amber-800", title: "Opgeslagen in dit systeem, wacht nog op doorgifte naar Floriday", children: "Wacht op Floriday" }), waitCause && (_jsx("span", { className: "text-[11px] text-muted-foreground", children: waitCause }))] })), low && (_jsx("div", { className: "text-xs text-destructive", children: "onder minimumvoorraad" })), overReserved && (_jsx("div", { className: "text-xs text-amber-700", title: "Er is meer gereserveerd voor orders dan er ligt. Controleer open orders.", children: "gereserveerd groter dan liggend" }))] }), _jsx("td", { className: "p-3", onClick: (e) => e.stopPropagation(), children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(InventoryStatusBadge, { setting: setting }), settingProductId && onOpenSettings && (_jsx(Button, { variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => onOpenSettings(settingProductId), "aria-label": `Voorraadsturing van ${item.name}`, children: _jsx(Settings2, { className: "h-4 w-4" }) }))] }) }), showType && _jsx("td", { className: "p-3", children: (() => {
159
164
  const t = toStructureType(setting?.product_type, item.itemType);
160
165
  return (_jsxs("div", { className: "flex flex-col items-start gap-1", children: [_jsx(Badge, { variant: t === "NORMAL" ? "secondary" : undefined, className: PRODUCT_TYPE_CLASSES[t], title: PRODUCT_TYPE_HELP[t], children: PRODUCT_TYPE_LABELS[t] }), setting?.action_reason === "GROUP_MEMBER_OFF" && (_jsx("span", { className: "text-[11px] text-muted-foreground", title: "Dit artikel is lid van een actieve productgroep; alleen de groep staat in Floriday.", children: "Lid van groep \u00B7 niet los in Floriday" })), setting?.action_reason === "SET_AWAITS_LOGISTICS" && (item.floridayId ? (_jsx("span", { className: "text-[11px] text-amber-700", title: "Set: dit artikel staat in Floriday. De voorraad (berekend uit de onderdelen) gaat pas naar Floriday na akkoord, als de Logistieke engine onderdelen reserveert.", children: "In Floriday \u00B7 voorraad van de samenstelling volgt" })) : (_jsx("span", { className: "text-[11px] text-amber-700", title: "Berekend uit de onderdelen; nog niet gekoppeld aan een Floriday-artikel.", children: "Berekend \u00B7 nog niet gekoppeld aan Floriday" })))] }));
161
166
  })() }), showArticle && _jsx("td", { className: "p-3 font-mono text-xs", children: item.articleNumber || "—" }), shownColumns.map((c) => (_jsx("td", { className: "p-3 text-right tabular-nums text-muted-foreground", children: item.stockMissing && item.itemType !== "productgroep"
@@ -0,0 +1,7 @@
1
+ import { type FloridayWriteMode } from "../lib/floriday-write-mode.js";
2
+ /**
3
+ * Schrijfstand van Floriday. Geeft undefined tijdens laden, null als lezen
4
+ * mislukt (dan toont het scherm "oorzaak onbekend"), anders de stand.
5
+ */
6
+ export declare function useFloridayWriteMode(enabled?: boolean): FloridayWriteMode | null | undefined;
7
+ //# sourceMappingURL=useFloridayWriteMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFloridayWriteMode.d.ts","sourceRoot":"","sources":["../../src/hooks/useFloridayWriteMode.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE5F;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,UAAO,GAAG,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAYzF"}
@@ -0,0 +1,21 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { fetchFloridayWriteMode } from "../lib/floriday-write-mode.js";
3
+ /**
4
+ * Schrijfstand van Floriday. Geeft undefined tijdens laden, null als lezen
5
+ * mislukt (dan toont het scherm "oorzaak onbekend"), anders de stand.
6
+ */
7
+ export function useFloridayWriteMode(enabled = true) {
8
+ const q = useQuery({
9
+ queryKey: ["floriday-write-mode"],
10
+ queryFn: fetchFloridayWriteMode,
11
+ enabled,
12
+ staleTime: 60000,
13
+ refetchOnWindowFocus: true,
14
+ retry: 1,
15
+ });
16
+ if (!enabled)
17
+ return undefined;
18
+ if (q.isError)
19
+ return null;
20
+ return q.data;
21
+ }
@@ -9,6 +9,15 @@ export interface StockSyncHealth {
9
9
  articleCount: number;
10
10
  /** Aantal van die artikelen zonder prijs in het weekaanbod. */
11
11
  noPriceCount: number;
12
+ /** Artikelen met regels die door het verpakkingsverschil zijn tegengehouden. */
13
+ blocked: BlockedArticle[];
14
+ /** Tegengehouden regels zonder bekend artikel. */
15
+ blockedWithoutProduct: number;
16
+ }
17
+ export interface BlockedArticle {
18
+ productId: string;
19
+ name: string | null;
20
+ code: string | null;
12
21
  }
13
22
  export declare function reasonLabel(error: string | null | undefined): string | null;
14
23
  export declare function useStockSyncHealth(): import("@tanstack/react-query").UseQueryResult<StockSyncHealth, Error>;
@@ -1 +1 @@
1
- {"version":3,"file":"useStockSyncHealth.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockSyncHealth.ts"],"names":[],"mappings":"AAeA,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;CACtB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAS3E;AAED,wBAAgB,kBAAkB,2EA6EjC"}
1
+ {"version":3,"file":"useStockSyncHealth.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockSyncHealth.ts"],"names":[],"mappings":"AAeA,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,2EAuGjC"}
@@ -88,6 +88,27 @@ export function useStockSyncHealth() {
88
88
  const withPrice = new Set((priced ?? []).map((p) => p.trade_item_id));
89
89
  noPriceCount = ids.filter((id) => !withPrice.has(id)).length;
90
90
  }
91
+ // Regels tegengehouden door het verpakkingsverschil (alleen lezen).
92
+ const { data: gateRows } = await supabase
93
+ .from("floriday_continuous_stock_queue")
94
+ .select("product_id, trade_item_id")
95
+ .eq("status", "blocked_by_gate")
96
+ .limit(500);
97
+ const gateProductIds = Array.from(new Set((gateRows ?? []).map((r) => r.product_id).filter(Boolean)));
98
+ const gateNoProduct = (gateRows ?? []).filter((r) => !r.product_id).length;
99
+ let blocked = [];
100
+ if (gateProductIds.length > 0) {
101
+ const { data: prods } = await supabase
102
+ .from("products")
103
+ .select("id, product, article_number")
104
+ .in("id", gateProductIds.slice(0, 200));
105
+ const byId = new Map((prods ?? []).map((p) => [p.id, p]));
106
+ blocked = gateProductIds.slice(0, 200).map((id) => ({
107
+ productId: id,
108
+ name: byId.get(id)?.product ?? null,
109
+ code: byId.get(id)?.article_number ?? null,
110
+ }));
111
+ }
91
112
  return {
92
113
  failed,
93
114
  pending,
@@ -96,6 +117,8 @@ export function useStockSyncHealth() {
96
117
  topReason,
97
118
  articleCount: ids.length,
98
119
  noPriceCount,
120
+ blocked,
121
+ blockedWithoutProduct: gateNoProduct,
99
122
  };
100
123
  },
101
124
  });
@@ -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;;;EAyFnC;AAED,wBAAsB,aAAa,CACjC,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAezC"}
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"}