@flowselections/floriday-voorraad 1.0.75 → 1.0.77

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.
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
2
3
  import { useNavigate } from "@tanstack/react-router";
3
- import { Package, Settings2 } from "lucide-react";
4
- import { Badge, Button, Checkbox } from "@flowselections/core";
4
+ import { Columns3, Package, Settings2 } from "lucide-react";
5
+ import { Badge, Button, Checkbox, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@flowselections/core";
6
+ import { supabase } from "../../integrations/supabase/client";
5
7
  import { useVoorraadCustomFields } from "../../hooks/useVoorraadCustomFields";
6
8
  import { formatCustomValue } from "../../lib/voorraad-custom-fields";
7
9
  import { InventoryStatusBadge } from "./InventoryStatusBadge";
@@ -14,6 +16,69 @@ const columns = [
14
16
  { key: "incomingQuantity", label: "Inkomend" },
15
17
  { key: "economicQuantity", label: "Economisch" },
16
18
  ];
19
+ /** Standaard zichtbare kolommen; de rest via "Kolommen kiezen" (per browser onthouden). */
20
+ const DEFAULT_VISIBLE = ["quantity", "availableQuantity", "price"];
21
+ const COLS_STORAGE_KEY = "voorraad.visibleColumns.v1";
22
+ const OPTIONAL_LABELS = {
23
+ type: "Type",
24
+ articleNumber: "Artikelnummer",
25
+ price: "Prijs",
26
+ };
27
+ function useVisibleColumns() {
28
+ const [visible, setVisible] = useState(new Set(DEFAULT_VISIBLE));
29
+ useEffect(() => {
30
+ try {
31
+ const raw = window.localStorage.getItem(COLS_STORAGE_KEY);
32
+ if (raw)
33
+ setVisible(new Set(JSON.parse(raw)));
34
+ }
35
+ catch {
36
+ /* standaard houden */
37
+ }
38
+ }, []);
39
+ const toggle = (key) => setVisible((cur) => {
40
+ const next = new Set(cur);
41
+ if (next.has(key))
42
+ next.delete(key);
43
+ else
44
+ next.add(key);
45
+ try {
46
+ window.localStorage.setItem(COLS_STORAGE_KEY, JSON.stringify([...next]));
47
+ }
48
+ catch {
49
+ /* niet opslaan is geen probleem */
50
+ }
51
+ return next;
52
+ });
53
+ return { visible, toggle };
54
+ }
55
+ /** Verkoopprijs per artikel (op id), alleen geladen als de kolom zichtbaar is. */
56
+ function useSalePrices(productIds, enabled) {
57
+ const [prices, setPrices] = useState(new Map());
58
+ const key = productIds.join(",");
59
+ useEffect(() => {
60
+ if (!enabled || productIds.length === 0)
61
+ return;
62
+ let cancelled = false;
63
+ void supabase
64
+ .from("products")
65
+ .select("id,sale_price")
66
+ .in("id", productIds)
67
+ .then(({ data }) => {
68
+ if (cancelled)
69
+ return;
70
+ const m = new Map();
71
+ for (const p of data ?? [])
72
+ m.set(p.id, p.sale_price == null ? null : Number(p.sale_price));
73
+ setPrices(m);
74
+ });
75
+ return () => {
76
+ cancelled = true;
77
+ };
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ }, [key, enabled]);
80
+ return prices;
81
+ }
17
82
  function customValue(item, key) {
18
83
  const cf = item.raw?.custom_fields;
19
84
  return cf && typeof cf === "object" ? cf[key] : undefined;
@@ -25,7 +90,14 @@ export function settingKeyOf(item) {
25
90
  export function VoorraadTable({ items, loading, settingsByKey, selected, onToggleSelect, onToggleAll, onOpenSettings, }) {
26
91
  const navigate = useNavigate();
27
92
  const { fields } = useVoorraadCustomFields();
28
- const extraColumns = fields.filter((f) => f.show_in_columns);
93
+ const { visible, toggle } = useVisibleColumns();
94
+ const allExtraColumns = fields.filter((f) => f.show_in_columns);
95
+ const extraColumns = allExtraColumns.filter((f) => visible.has(`cf:${f.id}`));
96
+ const shownColumns = columns.filter((c) => visible.has(c.key));
97
+ const showType = visible.has("type");
98
+ const showArticle = visible.has("articleNumber");
99
+ const showPrice = visible.has("price");
100
+ const prices = useSalePrices(items.map((i) => i.productId).filter((v) => Boolean(v)), showPrice);
29
101
  const selectableIds = items
30
102
  .map((i) => settingsByKey?.get(settingKeyOf(i) ?? "")?.product_id ?? null)
31
103
  .filter((v) => Boolean(v));
@@ -34,37 +106,48 @@ export function VoorraadTable({ items, loading, settingsByKey, selected, onToggl
34
106
  return _jsx("p", { className: "text-sm text-muted-foreground p-6", children: "Voorraad laden..." });
35
107
  }
36
108
  if (items.length === 0) {
37
- return (_jsx("p", { className: "text-sm text-muted-foreground p-6", children: "Geen producten gevonden voor deze connectie." }));
109
+ return (_jsx("p", { className: "text-sm text-muted-foreground p-6", children: "Geen producten gevonden voor deze Floriday-koppeling." }));
38
110
  }
39
- return (_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: [onToggleSelect && (_jsx("th", { className: "p-3 w-10", children: _jsx(Checkbox, { checked: allSelected, onCheckedChange: () => onToggleAll?.(selectableIds), "aria-label": "Alles selecteren" }) })), _jsx("th", { className: "p-3 w-14" }), _jsx("th", { className: "p-3", children: "Product" }), _jsx("th", { className: "p-3", children: "Floriday-sturing" }), _jsx("th", { className: "p-3", children: "Type" }), _jsx("th", { className: "p-3", children: "Artikelnummer" }), columns.map((c) => (_jsx("th", { className: "p-3 text-right whitespace-nowrap", children: c.label }, c.key))), extraColumns.map((f) => (_jsx("th", { className: "p-3 whitespace-nowrap", children: f.field_label }, f.id)))] }) }), _jsx("tbody", { children: items.map((item) => {
40
- const low = item.minQuantity > 0 &&
41
- item.availableQuantity < item.minQuantity;
42
- const overReserved = Number(item.reservedQuantity ?? 0) > Number(item.quantity ?? 0);
43
- const linkId = item.productId ?? item.localId ?? item.id;
44
- const clickable = Boolean(linkId);
45
- const open = () => {
46
- if (linkId)
47
- navigate({ to: "/voorraad/$id", params: { id: linkId } });
48
- };
49
- const floriday = item.floriday ?? null;
50
- const thumbUrl = floriday?.photoUrl || item.imageUrl;
51
- const setting = settingsByKey?.get(settingKeyOf(item) ?? "");
52
- const settingProductId = setting?.product_id ?? null;
53
- return (_jsxs("tr", { className: `border-t hover:bg-muted/30 ${clickable ? "cursor-pointer" : ""}`, onClick: clickable ? open : undefined, role: clickable ? "button" : undefined, tabIndex: clickable ? 0 : undefined, onKeyDown: clickable
54
- ? (e) => {
55
- if (e.key === "Enter" || e.key === " ") {
56
- e.preventDefault();
57
- open();
58
- }
59
- }
60
- : undefined, "aria-label": clickable ? `Bewerk ${item.name}` : undefined, children: [onToggleSelect && (_jsx("td", { className: "p-3", onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: settingProductId ? selected?.has(settingProductId) : false, disabled: !settingProductId, onCheckedChange: () => settingProductId && onToggleSelect(settingProductId), "aria-label": `Selecteer ${item.name}` }) })), _jsx("td", { className: "p-2", children: thumbUrl ? (_jsx("img", { src: thumbUrl, alt: item.name, className: "h-10 w-10 rounded object-cover", loading: "lazy" })) : (_jsx("div", { className: "h-10 w-10 rounded bg-muted flex items-center justify-center", children: _jsx(Package, { className: "h-4 w-4 text-muted-foreground" }) })) }), _jsxs("td", { className: "p-3", children: [_jsxs("div", { className: "flex flex-col items-start gap-1", children: [_jsx("span", { className: "font-medium text-foreground", children: item.name }), _jsxs("div", { role: "status", "aria-label": floriday
61
- ? "Online in Floriday"
62
- : "Niet online in Floriday", title: floriday
63
- ? "Online in Floriday"
64
- : "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 " +
65
- (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" }) }))] }) }), _jsx("td", { className: "p-3", children: (() => {
66
- const t = toStructureType(setting?.product_type, item.itemType);
67
- 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 nog niet doorgezet (set)" })) : (_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" })))] }));
68
- })() }), _jsx("td", { className: "p-3 font-mono text-xs", children: item.articleNumber || "—" }), columns.map((c) => (_jsx("td", { className: "p-3 text-right tabular-nums text-muted-foreground", children: Number(item[c.key] ?? 0) }, c.key))), extraColumns.map((f) => (_jsx("td", { className: "p-3 text-muted-foreground whitespace-nowrap", children: formatCustomValue(customValue(item, f.field_key)) }, f.id)))] }, item.id));
69
- }) })] }) }));
111
+ return (_jsxs("div", { children: [_jsx("div", { className: "flex justify-end px-3 pt-2", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Columns3, { className: "h-4 w-4 mr-1" }), " Kolommen kiezen"] }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuLabel, { children: "Kolommen" }), _jsx(DropdownMenuSeparator, {}), [
112
+ { key: "type", label: OPTIONAL_LABELS.type },
113
+ { key: "articleNumber", label: OPTIONAL_LABELS.articleNumber },
114
+ ...columns.map((c) => ({ key: c.key, label: c.label })),
115
+ { key: "price", label: OPTIONAL_LABELS.price },
116
+ ...allExtraColumns.map((f) => ({ key: `cf:${f.id}`, label: f.field_label })),
117
+ ].map((c) => (_jsx(DropdownMenuCheckboxItem, { checked: visible.has(c.key), onCheckedChange: () => toggle(c.key), onSelect: (e) => e.preventDefault(), children: c.label }, c.key)))] })] }) }), _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: [onToggleSelect && (_jsx("th", { className: "p-3 w-10", children: _jsx(Checkbox, { checked: allSelected, onCheckedChange: () => onToggleAll?.(selectableIds), "aria-label": "Alles selecteren" }) })), _jsx("th", { className: "p-3 w-14" }), _jsx("th", { className: "p-3", children: "Product" }), _jsx("th", { className: "p-3", children: "Floriday-sturing" }), showType && _jsx("th", { className: "p-3", children: "Type" }), showArticle && _jsx("th", { className: "p-3", children: "Artikelnummer" }), shownColumns.map((c) => (_jsx("th", { className: "p-3 text-right whitespace-nowrap", children: c.label }, c.key))), showPrice && _jsx("th", { className: "p-3 text-right whitespace-nowrap", children: "Prijs" }), extraColumns.map((f) => (_jsx("th", { className: "p-3 whitespace-nowrap", children: f.field_label }, f.id)))] }) }), _jsx("tbody", { children: items.map((item) => {
118
+ const low = item.minQuantity > 0 &&
119
+ item.availableQuantity < item.minQuantity;
120
+ const overReserved = Number(item.reservedQuantity ?? 0) > Number(item.quantity ?? 0);
121
+ const linkId = item.productId ?? item.localId ?? item.id;
122
+ const clickable = Boolean(linkId);
123
+ const open = () => {
124
+ if (linkId)
125
+ navigate({ to: "/voorraad/$id", params: { id: linkId } });
126
+ };
127
+ const floriday = item.floriday ?? null;
128
+ const thumbUrl = floriday?.photoUrl || item.imageUrl;
129
+ const setting = settingsByKey?.get(settingKeyOf(item) ?? "");
130
+ const settingProductId = setting?.product_id ?? null;
131
+ return (_jsxs("tr", { className: `border-t hover:bg-muted/30 ${clickable ? "cursor-pointer" : ""}`, onClick: clickable ? open : undefined, role: clickable ? "button" : undefined, tabIndex: clickable ? 0 : undefined, onKeyDown: clickable
132
+ ? (e) => {
133
+ if (e.key === "Enter" || e.key === " ") {
134
+ e.preventDefault();
135
+ open();
136
+ }
137
+ }
138
+ : undefined, "aria-label": clickable ? `Bewerk ${item.name}` : undefined, children: [onToggleSelect && (_jsx("td", { className: "p-3", onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: settingProductId ? selected?.has(settingProductId) : false, disabled: !settingProductId, onCheckedChange: () => settingProductId && onToggleSelect(settingProductId), "aria-label": `Selecteer ${item.name}` }) })), _jsx("td", { className: "p-2", children: thumbUrl ? (_jsx("img", { src: thumbUrl, alt: item.name, className: "h-10 w-10 rounded object-cover", loading: "lazy" })) : (_jsx("div", { className: "h-10 w-10 rounded bg-muted flex items-center justify-center", children: _jsx(Package, { className: "h-4 w-4 text-muted-foreground" }) })) }), _jsxs("td", { className: "p-3", children: [_jsxs("div", { className: "flex flex-col items-start gap-1", children: [_jsx("span", { className: "font-medium text-foreground", children: item.name }), _jsxs("div", { role: "status", "aria-label": floriday
139
+ ? "Online in Floriday"
140
+ : "Niet online in Floriday", title: floriday
141
+ ? "Online in Floriday"
142
+ : "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 " +
143
+ (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: (() => {
144
+ const t = toStructureType(setting?.product_type, item.itemType);
145
+ 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" })))] }));
146
+ })() }), 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: Number(item[c.key] ?? 0) }, c.key))), showPrice && (_jsx("td", { className: "p-3 text-right tabular-nums text-muted-foreground whitespace-nowrap", children: (() => {
147
+ const p = item.productId ? prices.get(item.productId) : undefined;
148
+ if (p == null)
149
+ return "—";
150
+ return `€ ${p.toLocaleString("nl-NL", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
151
+ })() })), extraColumns.map((f) => (_jsx("td", { className: "p-3 text-muted-foreground whitespace-nowrap", children: formatCustomValue(customValue(item, f.field_key)) }, f.id)))] }, item.id));
152
+ }) })] }) })] }));
70
153
  }