@flowselections/floriday-voorraad 1.0.34 → 1.0.35

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 +1 @@
1
- {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AAcA,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,+BA2HzF"}
1
+ {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AAeA,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,+BAkQzF"}
@@ -1,16 +1,73 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { Button, Card, CardContent, Input, Label, Textarea, toast, } from "@flowselections/core";
4
- import { Loader2, Package, Check } from "lucide-react";
4
+ import { Loader2, Package, Check, Factory } from "lucide-react";
5
5
  import { useTradeItemStock, stockKeysFor } from "../../hooks/useTradeItemStock";
6
6
  import { useFloridayContinuousStock } from "../../hooks/useFloridayContinuousStock";
7
+ import { supabase } from "../../integrations/supabase/client";
7
8
  export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productId }) {
8
- const keys = stockKeysFor({ tradeItemId, draftId, productId });
9
+ // De canonieke sleutel van een gekoppeld product staat in de producttabel
10
+ // (Floriday-UUID of `trade_item_key`). Zonder die sleutel zou de kaart op
11
+ // `product:<id>` schrijven terwijl het overzicht de kale sleutel leest.
12
+ const [productKeys, setProductKeys] = useState({ floridayId: null, tradeItemKey: null });
13
+ useEffect(() => {
14
+ if (!productId) {
15
+ setProductKeys({ floridayId: null, tradeItemKey: null });
16
+ return;
17
+ }
18
+ let cancelled = false;
19
+ (async () => {
20
+ const { data } = await supabase
21
+ .from("products")
22
+ .select("trade_item_key,floriday_trade_item_id")
23
+ .eq("id", productId)
24
+ .maybeSingle();
25
+ if (cancelled)
26
+ return;
27
+ setProductKeys({
28
+ floridayId: data?.floriday_trade_item_id ?? null,
29
+ tradeItemKey: data?.trade_item_key ?? null,
30
+ });
31
+ })();
32
+ return () => {
33
+ cancelled = true;
34
+ };
35
+ }, [productId]);
36
+ const keys = stockKeysFor({
37
+ tradeItemId: tradeItemId ?? productKeys.floridayId,
38
+ tradeItemKey: productKeys.tradeItemKey,
39
+ draftId,
40
+ productId,
41
+ });
9
42
  const key = keys[0] ?? null;
10
43
  const { row, loading, save } = useTradeItemStock(connectionId, keys);
11
44
  const [values, setValues] = useState(row);
12
45
  const [saving, setSaving] = useState(false);
13
46
  const [touched, setTouched] = useState(false);
47
+ const [hasBom, setHasBom] = useState(false);
48
+ const [produceQty, setProduceQty] = useState("");
49
+ const [producing, setProducing] = useState(false);
50
+ // Heeft dit artikel onderdelen (samenstelling)? Alleen dan tonen we de
51
+ // knop "Produceren"; handmatig bijstellen blijft altijd een correctie.
52
+ useEffect(() => {
53
+ let cancelled = false;
54
+ (async () => {
55
+ if (!productId && !tradeItemId) {
56
+ setHasBom(false);
57
+ return;
58
+ }
59
+ let query = supabase.from("product_bom_items").select("id").limit(1);
60
+ query = productId
61
+ ? query.eq("parent_product_id", productId)
62
+ : query.eq("parent_trade_item_floriday_id", tradeItemId);
63
+ const { data } = await query;
64
+ if (!cancelled)
65
+ setHasBom((data ?? []).length > 0);
66
+ })();
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [productId, tradeItemId]);
14
71
  const floridayPieces = useFloridayContinuousStock(connectionId, tradeItemId ?? null);
15
72
  useEffect(() => {
16
73
  setValues(row);
@@ -29,22 +86,64 @@ export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productI
29
86
  return (_jsx(Card, { className: "p-4 text-sm text-muted-foreground", children: "Voorraad kan pas worden ingesteld nadat het artikel voor het eerst is opgeslagen." }));
30
87
  }
31
88
  const available = Math.max(0, values.physical_stock - values.reserved_stock);
89
+ // Foutmeldingen uit de database zijn soms JSON of technisch; hier maken we
90
+ // er gewone taal van zodat de gebruiker weet wat er moet gebeuren.
91
+ const humanError = (e) => {
92
+ const raw = e?.message ?? String(e);
93
+ try {
94
+ const parsed = JSON.parse(raw);
95
+ const msg = parsed?.message ?? parsed?.error ?? raw;
96
+ if (typeof msg === "string" && msg.toLowerCase().includes("onvoldoende voorraad")) {
97
+ return "Onvoldoende voorraad van een onderdeel. Gebruik 'Produceren' om onderdelen af te schrijven, of pas de voorraad van het onderdeel eerst aan.";
98
+ }
99
+ return typeof msg === "string" ? msg : raw;
100
+ }
101
+ catch {
102
+ return raw;
103
+ }
104
+ };
32
105
  const handleSave = async () => {
33
106
  setSaving(true);
34
107
  try {
35
108
  await save(values);
36
- toast.success("Voorraad bijgewerkt");
109
+ toast.success("Voorraad gecorrigeerd");
37
110
  }
38
111
  catch (e) {
39
- toast.error(`Opslaan mislukt: ${e.message}`);
112
+ toast.error(`Opslaan mislukt: ${humanError(e)}`);
40
113
  }
41
114
  finally {
42
115
  setSaving(false);
43
116
  }
44
117
  };
118
+ const handleProduce = async () => {
119
+ const qty = Number(produceQty);
120
+ if (!Number.isFinite(qty) || qty <= 0) {
121
+ toast.error("Vul een aantal groter dan 0 in");
122
+ return;
123
+ }
124
+ setProducing(true);
125
+ try {
126
+ const { error } = await supabase.rpc("produce_designed_product", {
127
+ p_parent_kind: productId ? "product" : "floriday",
128
+ p_parent_id: (productId ?? tradeItemId),
129
+ p_quantity: qty,
130
+ });
131
+ if (error)
132
+ throw new Error(error.message);
133
+ toast.success(`${qty} stuks geproduceerd; onderdelen afgeschreven`);
134
+ setProduceQty("");
135
+ setTouched(false);
136
+ }
137
+ catch (e) {
138
+ toast.error(`Produceren mislukt: ${humanError(e)}`);
139
+ }
140
+ finally {
141
+ setProducing(false);
142
+ }
143
+ };
45
144
  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, value: String(values[id] ?? 0), onChange: (e) => {
46
145
  setTouched(true);
47
146
  setValues((v) => ({ ...v, [id]: Number(e.target.value) || 0 }));
48
147
  } })] }));
49
- return (_jsxs(Card, { children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-6 py-4", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full border border-primary/40 bg-primary/10 text-primary", children: _jsx(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: [_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 ?? "", onChange: (e) => setValues((v) => ({ ...v, notes: e.target.value })), placeholder: "Bijv. locatie of opmerking" })] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { onClick: handleSave, disabled: saving, children: [saving ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Voorraad opslaan"] }) })] })) })] }));
148
+ return (_jsxs(Card, { children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-6 py-4", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full border border-primary/40 bg-primary/10 text-primary", children: _jsx(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: [_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 ?? "", onChange: (e) => setValues((v) => ({ ...v, notes: e.target.value })), placeholder: "Bijv. locatie of opmerking" })] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { onClick: handleSave, disabled: saving, children: [saving ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Check, { className: "mr-2 h-4 w-4" })), "Voorraad opslaan"] }) }), hasBom && (_jsxs("div", { className: "space-y-2 rounded-md border border-border bg-muted/30 p-3", children: [_jsx("p", { className: "text-sm font-medium", children: "Produceren" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Handmatig aanpassen hierboven is een correctie: onderdelen blijven ongemoeid. Wil je onderdelen w\u00E9l afschrijven, geef dan hier het geproduceerde aantal op." }), _jsxs("div", { className: "flex items-end gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "produce-qty", children: "Aantal" }), _jsx(Input, { id: "produce-qty", type: "number", min: 1, className: "w-32", value: produceQty, onChange: (e) => setProduceQty(e.target.value) })] }), _jsxs(Button, { variant: "outline", onClick: handleProduce, disabled: producing, children: [producing ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Factory, { className: "mr-2 h-4 w-4" })), "Produceren"] })] })] }))] })) })] }));
50
149
  }
@@ -8,18 +8,20 @@ export interface TradeItemStockRow {
8
8
  }
9
9
  export declare function stockKeyFor(params: {
10
10
  tradeItemId?: string | null;
11
+ tradeItemKey?: string | null;
11
12
  draftId?: string | null;
12
13
  productId?: string | null;
13
14
  }): string | null;
14
15
  /**
15
- * Build the ordered list of candidate keys for one artikel. Wij zoeken op
16
- * ALLE bekende varianten (tradeItemId, product:{id}, draft:{id}) omdat de
17
- * voorraadrij historisch onder één van die keys kan zijn aangemaakt (bv.
18
- * concept → gepubliceerd, of gelinkt aan een ERP-product). Bij opslaan
19
- * gebruiken wij altijd de eerste (meest specifieke) key.
16
+ * Eén gedeelde sleutelvolgorde voor de hele module: Floriday-UUID →
17
+ * `products.trade_item_key` → `product:<id>` → `draft:<id>`. Alle schermen
18
+ * (kaart, overzicht, import, wizard) gebruiken deze volgorde, zodat ze nooit
19
+ * een andere voorraadrij kunnen kiezen. De eerste sleutel is tevens de
20
+ * canonieke sleutel waarop wordt opgeslagen.
20
21
  */
21
22
  export declare function stockKeysFor(params: {
22
23
  tradeItemId?: string | null;
24
+ tradeItemKey?: string | null;
23
25
  draftId?: string | null;
24
26
  productId?: string | null;
25
27
  }): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"useTradeItemStock.d.ts","sourceRoot":"","sources":["../../src/hooks/useTradeItemStock.ts"],"names":[],"mappings":"AAGA,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,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,GAAG,IAAI,CAKhB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,EAAE,CAMX;AAED;;;;;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;;;mBA4ElB,iBAAiB;EAmEnC;AAED,wBAAsB,aAAa,CACjC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAUzC"}
1
+ {"version":3,"file":"useTradeItemStock.d.ts","sourceRoot":"","sources":["../../src/hooks/useTradeItemStock.ts"],"names":[],"mappings":"AAGA,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;;;mBA4ElB,iBAAiB;EAmEnC;AAED,wBAAsB,aAAa,CACjC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAUzC"}
@@ -8,29 +8,25 @@ const EMPTY = {
8
8
  notes: null,
9
9
  };
10
10
  export function stockKeyFor(params) {
11
- if (params.tradeItemId)
12
- return params.tradeItemId;
13
- if (params.draftId)
14
- return `draft:${params.draftId}`;
15
- if (params.productId)
16
- return `product:${params.productId}`;
17
- return null;
11
+ return stockKeysFor(params)[0] ?? null;
18
12
  }
19
13
  /**
20
- * Build the ordered list of candidate keys for one artikel. Wij zoeken op
21
- * ALLE bekende varianten (tradeItemId, product:{id}, draft:{id}) omdat de
22
- * voorraadrij historisch onder één van die keys kan zijn aangemaakt (bv.
23
- * concept → gepubliceerd, of gelinkt aan een ERP-product). Bij opslaan
24
- * gebruiken wij altijd de eerste (meest specifieke) key.
14
+ * Eén gedeelde sleutelvolgorde voor de hele module: Floriday-UUID →
15
+ * `products.trade_item_key` → `product:<id>` → `draft:<id>`. Alle schermen
16
+ * (kaart, overzicht, import, wizard) gebruiken deze volgorde, zodat ze nooit
17
+ * een andere voorraadrij kunnen kiezen. De eerste sleutel is tevens de
18
+ * canonieke sleutel waarop wordt opgeslagen.
25
19
  */
26
20
  export function stockKeysFor(params) {
27
21
  const keys = [];
28
- if (params.tradeItemId)
29
- keys.push(params.tradeItemId);
30
- if (params.productId)
31
- keys.push(`product:${params.productId}`);
32
- if (params.draftId)
33
- keys.push(`draft:${params.draftId}`);
22
+ const push = (k) => {
23
+ if (k && !keys.includes(k))
24
+ keys.push(k);
25
+ };
26
+ push(params.tradeItemId);
27
+ push(params.tradeItemKey);
28
+ push(params.productId ? `product:${params.productId}` : null);
29
+ push(params.draftId ? `draft:${params.draftId}` : null);
34
30
  return keys;
35
31
  }
36
32
  /**