@flowselections/floriday-voorraad 1.0.145 → 1.0.149

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 (100) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts +3 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  3. package/dist-lib/components/voorraad/ArtikelWizard.js +56 -52
  4. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
  5. package/dist-lib/components/voorraad/EditArtikelPage.js +15 -23
  6. package/dist-lib/components/voorraad/OrderHistoryCard.d.ts +3 -1
  7. package/dist-lib/components/voorraad/OrderHistoryCard.d.ts.map +1 -1
  8. package/dist-lib/components/voorraad/OrderHistoryCard.js +7 -3
  9. package/dist-lib/components/voorraad/ProductCustomFieldsCard.d.ts +5 -1
  10. package/dist-lib/components/voorraad/ProductCustomFieldsCard.d.ts.map +1 -1
  11. package/dist-lib/components/voorraad/ProductCustomFieldsCard.js +37 -18
  12. package/dist-lib/components/voorraad/ProductSalePriceField.d.ts.map +1 -1
  13. package/dist-lib/components/voorraad/ProductSalePriceField.js +11 -8
  14. package/dist-lib/components/voorraad/StepSamenstelling.d.ts.map +1 -1
  15. package/dist-lib/components/voorraad/StepSamenstelling.js +35 -10
  16. package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts +3 -1
  17. package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts.map +1 -1
  18. package/dist-lib/components/voorraad/StockMutationHistoryCard.js +16 -10
  19. package/dist-lib/components/voorraad/TrayStickerCard.d.ts +5 -1
  20. package/dist-lib/components/voorraad/TrayStickerCard.d.ts.map +1 -1
  21. package/dist-lib/components/voorraad/TrayStickerCard.js +64 -66
  22. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +0 -5
  23. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
  24. package/dist-lib/components/voorraad/ValidationOverviewTable.js +10 -4
  25. package/dist-lib/components/voorraad/VoorraadBeheerCard.d.ts +3 -0
  26. package/dist-lib/components/voorraad/VoorraadBeheerCard.d.ts.map +1 -0
  27. package/dist-lib/components/voorraad/VoorraadBeheerCard.js +44 -0
  28. package/dist-lib/components/voorraad/VoorraadPage.d.ts +1 -13
  29. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  30. package/dist-lib/components/voorraad/VoorraadPage.js +102 -140
  31. package/dist-lib/components/voorraad/VoorraadSturingCard.d.ts +6 -0
  32. package/dist-lib/components/voorraad/VoorraadSturingCard.d.ts.map +1 -0
  33. package/dist-lib/components/voorraad/VoorraadSturingCard.js +47 -0
  34. package/dist-lib/components/voorraad/artikel/ArtikelPagina.d.ts +6 -0
  35. package/dist-lib/components/voorraad/artikel/ArtikelPagina.d.ts.map +1 -0
  36. package/dist-lib/components/voorraad/artikel/ArtikelPagina.js +313 -0
  37. package/dist-lib/components/voorraad/artikel/TwijfelKiezen.d.ts +7 -0
  38. package/dist-lib/components/voorraad/artikel/TwijfelKiezen.d.ts.map +1 -0
  39. package/dist-lib/components/voorraad/artikel/TwijfelKiezen.js +54 -0
  40. package/dist-lib/components/voorraad/artikel/opslaan-context.d.ts +21 -0
  41. package/dist-lib/components/voorraad/artikel/opslaan-context.d.ts.map +1 -0
  42. package/dist-lib/components/voorraad/artikel/opslaan-context.js +15 -0
  43. package/dist-lib/components/voorraad/lijst/ImporteerKnop.d.ts +5 -0
  44. package/dist-lib/components/voorraad/lijst/ImporteerKnop.d.ts.map +1 -0
  45. package/dist-lib/components/voorraad/lijst/ImporteerKnop.js +20 -0
  46. package/dist-lib/components/voorraad/lijst/LijstExportKnop.d.ts +6 -0
  47. package/dist-lib/components/voorraad/lijst/LijstExportKnop.d.ts.map +1 -0
  48. package/dist-lib/components/voorraad/lijst/LijstExportKnop.js +43 -0
  49. package/dist-lib/components/voorraad/lijst/SnelActieDialogen.d.ts +16 -0
  50. package/dist-lib/components/voorraad/lijst/SnelActieDialogen.d.ts.map +1 -0
  51. package/dist-lib/components/voorraad/lijst/SnelActieDialogen.js +95 -0
  52. package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.d.ts +21 -0
  53. package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.d.ts.map +1 -0
  54. package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.js +39 -0
  55. package/dist-lib/hooks/useVoorraadCustomFields.d.ts +2 -1
  56. package/dist-lib/hooks/useVoorraadCustomFields.d.ts.map +1 -1
  57. package/dist-lib/hooks/useVoorraadCustomFields.js +8 -4
  58. package/dist-lib/hooks/useVoorraadLijst.d.ts +21 -0
  59. package/dist-lib/hooks/useVoorraadLijst.d.ts.map +1 -0
  60. package/dist-lib/hooks/useVoorraadLijst.js +71 -0
  61. package/dist-lib/index.d.ts.map +1 -1
  62. package/dist-lib/index.js +4 -0
  63. package/dist-lib/integrations/supabase/types.d.ts +748 -1
  64. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  65. package/dist-lib/lib/artikel-gewijzigd.d.ts +4 -0
  66. package/dist-lib/lib/artikel-gewijzigd.d.ts.map +1 -0
  67. package/dist-lib/lib/artikel-gewijzigd.js +7 -0
  68. package/dist-lib/lib/artikel-pagina.d.ts +68 -0
  69. package/dist-lib/lib/artikel-pagina.d.ts.map +1 -0
  70. package/dist-lib/lib/artikel-pagina.js +97 -0
  71. package/dist-lib/lib/count-stock.d.ts +17 -0
  72. package/dist-lib/lib/count-stock.d.ts.map +1 -0
  73. package/dist-lib/lib/count-stock.js +26 -0
  74. package/dist-lib/lib/floriday-import-validator.d.ts +9 -0
  75. package/dist-lib/lib/floriday-import-validator.d.ts.map +1 -1
  76. package/dist-lib/lib/floriday-import-validator.js +13 -0
  77. package/dist-lib/lib/floriday-versie-ronde.d.ts +59 -0
  78. package/dist-lib/lib/floriday-versie-ronde.d.ts.map +1 -0
  79. package/dist-lib/lib/floriday-versie-ronde.functions.d.ts +10 -0
  80. package/dist-lib/lib/floriday-versie-ronde.functions.d.ts.map +1 -0
  81. package/dist-lib/lib/floriday-versie-ronde.functions.js +22 -0
  82. package/dist-lib/lib/floriday-versie-ronde.js +106 -0
  83. package/dist-lib/lib/pending-photos.d.ts +20 -0
  84. package/dist-lib/lib/pending-photos.d.ts.map +1 -0
  85. package/dist-lib/lib/pending-photos.js +90 -0
  86. package/dist-lib/lib/voorraad-custom-fields.d.ts +16 -1
  87. package/dist-lib/lib/voorraad-custom-fields.d.ts.map +1 -1
  88. package/dist-lib/lib/voorraad-custom-fields.js +12 -6
  89. package/dist-lib/lib/voorraad-lijst.d.ts +92 -0
  90. package/dist-lib/lib/voorraad-lijst.d.ts.map +1 -0
  91. package/dist-lib/lib/voorraad-lijst.js +86 -0
  92. package/dist-lib/server-hooks/floriday-stock-reconcile.d.ts.map +1 -1
  93. package/dist-lib/server-hooks/floriday-stock-reconcile.js +5 -0
  94. package/dist-lib/server-hooks/floriday-versieronde.d.ts +2 -0
  95. package/dist-lib/server-hooks/floriday-versieronde.d.ts.map +1 -0
  96. package/dist-lib/server-hooks/floriday-versieronde.js +29 -0
  97. package/dist-lib/server-hooks/guard.js +1 -1
  98. package/package.json +1 -1
  99. package/sql/AGENTS.md +6 -0
  100. package/sql/floriday-voorraad-schema.sql +1355 -22
@@ -94,6 +94,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
94
94
  const [loading, setLoading] = useState(true);
95
95
  const [saving, setSaving] = useState(false);
96
96
  const [rows, setRows] = useState([]);
97
+ const [teVerwijderen, setTeVerwijderen] = useState([]);
97
98
  const [names, setNames] = useState(new Map());
98
99
  const [activeTab, setActiveTab] = useState("__base__");
99
100
  const hasParent = Boolean(productId || floridayId);
@@ -131,8 +132,10 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
131
132
  component_trade_item_floriday_id: r.component_trade_item_floriday_id ?? null,
132
133
  quantity: Number(r.quantity ?? 1),
133
134
  sort_order: Number(r.sort_order ?? 0),
135
+ _orig: { quantity: Number(r.quantity ?? 1), sort_order: Number(r.sort_order ?? 0) },
134
136
  }));
135
137
  setRows(list);
138
+ setTeVerwijderen([]);
136
139
  await resolveNames(list);
137
140
  setLoading(false);
138
141
  }, [productId, floridayId, hasParent]);
@@ -230,13 +233,9 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
230
233
  const row = rows.find((r) => r.id === id);
231
234
  if (!row)
232
235
  return;
233
- if (!row._new) {
234
- const { error } = await supabase.from("product_bom_items").delete().eq("id", id);
235
- if (error) {
236
- toast.error("Verwijderen mislukt");
237
- return;
238
- }
239
- }
236
+ // Verwijderen gebeurt pas bij "Opslaan" (bewuste actie met conflictcontrole).
237
+ if (!row._new)
238
+ setTeVerwijderen((l) => [...l, row]);
240
239
  setRows((prev) => prev.filter((r) => r.id !== id));
241
240
  };
242
241
  const saveAll = async () => {
@@ -245,7 +244,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
245
244
  return;
246
245
  }
247
246
  const dirty = rows.filter((r) => r._dirty);
248
- if (dirty.length === 0)
247
+ if (dirty.length === 0 && teVerwijderen.length === 0)
249
248
  return;
250
249
  setSaving(true);
251
250
  // Verhoudingscontrole: alle lagen meetellen, nooit blokkeren.
@@ -264,6 +263,32 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
264
263
  // Controle mislukt: opslaan gaat gewoon door.
265
264
  }
266
265
  try {
266
+ // Conflictcontrole: bestaande regels die je wijzigt of verwijdert moeten nog
267
+ // dezelfde stand hebben als bij openen.
268
+ const gecontroleerd = [...dirty.filter((r) => !r._new), ...teVerwijderen].filter((r) => r._orig);
269
+ if (gecontroleerd.length) {
270
+ const { data: nu, error: nuErr } = await supabase
271
+ .from("product_bom_items")
272
+ .select("id,quantity,sort_order")
273
+ .in("id", gecontroleerd.map((r) => r.id));
274
+ if (nuErr)
275
+ throw nuErr;
276
+ const huidig = new Map((nu ?? []).map((r) => [r.id, r]));
277
+ const afwijkend = gecontroleerd.filter((r) => {
278
+ const h = huidig.get(r.id);
279
+ return !h || Number(h.quantity) !== r._orig.quantity || Number(h.sort_order) !== r._orig.sort_order;
280
+ });
281
+ if (afwijkend.length) {
282
+ toast.error("Iemand anders heeft deze samenstelling intussen gewijzigd. Er is niets opgeslagen; laad de samenstelling opnieuw.");
283
+ return;
284
+ }
285
+ }
286
+ for (const r of teVerwijderen) {
287
+ const { error } = await supabase.from("product_bom_items").delete().eq("id", r.id);
288
+ if (error)
289
+ throw error;
290
+ }
291
+ setTeVerwijderen([]);
267
292
  const inserts = dirty
268
293
  .filter((r) => r._new)
269
294
  .map((r) => ({
@@ -309,7 +334,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
309
334
  if (error)
310
335
  throw error;
311
336
  }
312
- setRows((prev) => prev.map((r) => ({ ...r, _dirty: false })));
337
+ setRows((prev) => prev.map((r) => ({ ...r, _dirty: false, _orig: { quantity: r.quantity, sort_order: r.sort_order } })));
313
338
  toast.success("Samenstelling opgeslagen");
314
339
  }
315
340
  catch (e) {
@@ -319,7 +344,7 @@ export function StepSamenstelling({ productId, floridayId, packings, packagingTy
319
344
  setSaving(false);
320
345
  }
321
346
  };
322
- const hasUnsaved = rows.some((r) => r._dirty);
347
+ const hasUnsaved = rows.some((r) => r._dirty) || teVerwijderen.length > 0;
323
348
  if (!hasParent) {
324
349
  return (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground", children: "Sla het artikel eerst op om een samenstelling te configureren." }));
325
350
  }
@@ -1,7 +1,9 @@
1
1
  interface Props {
2
2
  tradeItemKey: string | null;
3
3
  productId?: string | null;
4
+ /** Sleutels uit voorraad_artikel_detail; scheelt een aparte aanvraag bij het openen. */
5
+ sleutels?: string[];
4
6
  }
5
- export declare function StockMutationHistoryCard({ tradeItemKey, productId }: Props): import("react").JSX.Element;
7
+ export declare function StockMutationHistoryCard({ tradeItemKey, productId, sleutels }: Props): import("react").JSX.Element;
6
8
  export {};
7
9
  //# sourceMappingURL=StockMutationHistoryCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StockMutationHistoryCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockMutationHistoryCard.tsx"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAiDD,wBAAgB,wBAAwB,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,KAAK,+BA4O1E"}
1
+ {"version":3,"file":"StockMutationHistoryCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockMutationHistoryCard.tsx"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAiDD,wBAAgB,wBAAwB,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,KAAK,+BAgPpF"}
@@ -42,7 +42,7 @@ function formatMoment(v) {
42
42
  return v;
43
43
  }
44
44
  }
45
- export function StockMutationHistoryCard({ tradeItemKey, productId }) {
45
+ export function StockMutationHistoryCard({ tradeItemKey, productId, sleutels }) {
46
46
  const [rows, setRows] = useState(null);
47
47
  const [error, setError] = useState(null);
48
48
  const [expanded, setExpanded] = useState(false);
@@ -60,14 +60,20 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
60
60
  keys.add(tradeItemKey);
61
61
  if (productId)
62
62
  keys.add(productId);
63
- // Groepssleutels waaronder dit artikel ook afgeboekt kan worden.
64
- const { data: groups } = await supabase
65
- .from("product_group_items")
66
- .select("product_group_id")
67
- .in("trade_item_id", [...keys])
68
- .limit(20);
69
- for (const g of (groups ?? [])) {
70
- keys.add(`group:${g.product_group_id}`);
63
+ // Groepssleutels waaronder dit artikel ook afgeboekt kan worden (meestal al meegeleverd).
64
+ if (sleutels) {
65
+ for (const k of sleutels)
66
+ keys.add(k);
67
+ }
68
+ else {
69
+ const { data: groups } = await supabase
70
+ .from("product_group_items")
71
+ .select("product_group_id")
72
+ .in("trade_item_id", [...keys])
73
+ .limit(20);
74
+ for (const g of (groups ?? [])) {
75
+ keys.add(`group:${g.product_group_id}`);
76
+ }
71
77
  }
72
78
  const { data, error } = await supabase
73
79
  .from("stock_mutation_log")
@@ -135,7 +141,7 @@ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
135
141
  cancelled = true;
136
142
  supabase.removeChannel(channel);
137
143
  };
138
- }, [tradeItemKey, productId]);
144
+ }, [tradeItemKey, productId, sleutels?.join("|")]); // eslint-disable-line react-hooks/exhaustive-deps
139
145
  return (_jsxs(Card, { children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-6 py-4", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full border border-primary/40 bg-primary/10 text-primary", children: _jsx(History, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Afboekgeschiedenis" }), _jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: rows ? `${rows.length} mutatie${rows.length === 1 ? "" : "s"}` : "" })] }), _jsxs(CardContent, { className: "pt-0", children: [error && (_jsx("div", { className: "text-sm text-destructive", children: error })), !rows && !error && (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Mutaties laden\u2026"] })), rows && rows.length === 0 && !error && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen voorraadmutaties voor dit artikel." })), rows && rows.length > 0 && (_jsxs("div", { className: "overflow-x-auto rounded-md border", children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-xs uppercase text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left", children: "Wanneer" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Soort" }), _jsx("th", { className: "px-3 py-2 text-right", children: "Aantal" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Van \u2192 naar" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Order" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Door" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Reden" })] }) }), _jsx("tbody", { children: (expanded ? rows : rows.slice(0, 5)).map((r) => {
140
146
  const meta = TYPE_LABEL[r.mutation_type] ?? {
141
147
  label: r.mutation_type || "Onbekend",
@@ -1,4 +1,8 @@
1
- /** Traysticker-afbeelding per product — los van de Floriday-productfoto (`products.image_url`). */
1
+ /**
2
+ * Traysticker-afbeelding per artikel — los van de Floriday-productfoto.
3
+ * Kiezen of verwijderen wordt pas bewaard met de knop "Opslaan" van de pagina,
4
+ * met controle tegen de stand bij openen.
5
+ */
2
6
  export declare function TrayStickerCard({ productId }: {
3
7
  productId: string;
4
8
  }): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"TrayStickerCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/TrayStickerCard.tsx"],"names":[],"mappings":"AAyBA,mGAAmG;AACnG,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,+BA+HnE"}
1
+ {"version":3,"file":"TrayStickerCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/TrayStickerCard.tsx"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,+BAoInE"}
@@ -1,100 +1,98 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, toast, } from "@flowselections/core";
4
- import { Loader2, Trash2, Upload } from "lucide-react";
3
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@flowselections/core";
4
+ import { Loader2, Trash2, Undo2, Upload } from "lucide-react";
5
5
  import { supabase } from "../../integrations/supabase/client.js";
6
+ import { useOpslagDeelnemer } from "./artikel/opslaan-context.js";
6
7
  const BUCKET = "product-tray-stickers";
7
8
  // Bucket is privé (publieke buckets zijn geblokkeerd): lange signed URL voor de logistiek-engine.
8
9
  const SIGNED_URL_SECONDS = 60 * 60 * 24 * 365 * 10;
9
- /** Traysticker-afbeelding per product — los van de Floriday-productfoto (`products.image_url`). */
10
+ /**
11
+ * Traysticker-afbeelding per artikel — los van de Floriday-productfoto.
12
+ * Kiezen of verwijderen wordt pas bewaard met de knop "Opslaan" van de pagina,
13
+ * met controle tegen de stand bij openen.
14
+ */
10
15
  export function TrayStickerCard({ productId }) {
11
16
  const [row, setRow] = useState(null);
12
17
  const [loading, setLoading] = useState(true);
13
- const [busy, setBusy] = useState(false);
18
+ const [wachtend, setWachtend] = useState(null);
14
19
  const inputRef = useRef(null);
15
- useEffect(() => {
16
- let cancelled = false;
17
- setLoading(true);
18
- supabase
20
+ const laad = async () => {
21
+ const { data } = await supabase
19
22
  .from("product_tray_stickers")
20
23
  .select("*")
21
24
  .eq("product_id", productId)
22
- .maybeSingle()
23
- .then(({ data }) => {
24
- if (!cancelled)
25
- setRow(data ?? null);
25
+ .maybeSingle();
26
+ setRow(data ?? null);
27
+ };
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+ setLoading(true);
31
+ void laad().finally(() => {
26
32
  if (!cancelled)
27
33
  setLoading(false);
28
34
  });
29
35
  return () => {
30
36
  cancelled = true;
31
37
  };
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
39
  }, [productId]);
33
- const upload = async (file) => {
34
- setBusy(true);
40
+ useEffect(() => () => {
41
+ if (wachtend?.soort === "nieuw")
42
+ URL.revokeObjectURL(wachtend.voorbeeld);
43
+ }, [wachtend]);
44
+ const opslaan = async () => {
45
+ if (!wachtend)
46
+ return { ok: true };
47
+ let pad = null;
35
48
  try {
36
- const ext = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".")) : "";
37
- const path = `${productId}/${crypto.randomUUID()}${ext}`;
38
- const up = await supabase.storage.from(BUCKET).upload(path, file, {
39
- contentType: file.type || undefined,
49
+ let next = null;
50
+ if (wachtend.soort === "nieuw") {
51
+ const file = wachtend.file;
52
+ const ext = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".")) : "";
53
+ pad = `${productId}/${crypto.randomUUID()}${ext}`;
54
+ const up = await supabase.storage.from(BUCKET).upload(pad, file, { contentType: file.type || undefined });
55
+ if (up.error)
56
+ throw up.error;
57
+ const signed = await supabase.storage.from(BUCKET).createSignedUrl(pad, SIGNED_URL_SECONDS);
58
+ if (signed.error || !signed.data)
59
+ throw signed.error ?? new Error("Geen URL");
60
+ next = { product_id: productId, bestand_url: signed.data.signedUrl, bestandspad: pad, bestandsnaam: file.name, bestandsgrootte: file.size };
61
+ }
62
+ const { data, error } = await supabase.rpc("voorraad_traysticker_opslaan", {
63
+ p_product_id: productId,
64
+ p_oud_pad: row?.bestandspad ?? null,
65
+ p_nieuw: next,
40
66
  });
41
- if (up.error)
42
- throw up.error;
43
- const signed = await supabase.storage.from(BUCKET).createSignedUrl(path, SIGNED_URL_SECONDS);
44
- if (signed.error || !signed.data)
45
- throw signed.error ?? new Error("Geen URL");
46
- const next = {
47
- product_id: productId,
48
- bestand_url: signed.data.signedUrl,
49
- bestandspad: path,
50
- bestandsnaam: file.name,
51
- bestandsgrootte: file.size,
52
- };
53
- const { error } = await supabase
54
- .from("product_tray_stickers")
55
- .upsert(next, { onConflict: "product_id" });
56
67
  if (error)
57
68
  throw error;
69
+ if (!data.ok) {
70
+ if (pad)
71
+ await supabase.storage.from(BUCKET).remove([pad]);
72
+ setWachtend(null);
73
+ await laad();
74
+ return { ok: false, melding: "Traysticker niet opgeslagen: iemand anders wijzigde hem intussen. De nieuwste stand is geladen." };
75
+ }
58
76
  if (row?.bestandspad)
59
77
  await supabase.storage.from(BUCKET).remove([row.bestandspad]);
60
78
  setRow(next);
61
- toast.success("Traysticker-afbeelding opgeslagen");
62
- }
63
- catch (e) {
64
- toast.error(`Uploaden mislukt: ${e.message}`);
65
- }
66
- finally {
67
- setBusy(false);
68
- if (inputRef.current)
69
- inputRef.current.value = "";
70
- }
71
- };
72
- const remove = async () => {
73
- if (!row)
74
- return;
75
- setBusy(true);
76
- try {
77
- const { error } = await supabase
78
- .from("product_tray_stickers")
79
- .delete()
80
- .eq("product_id", productId);
81
- if (error)
82
- throw error;
83
- if (row.bestandspad)
84
- await supabase.storage.from(BUCKET).remove([row.bestandspad]);
85
- setRow(null);
86
- toast.success("Traysticker-afbeelding verwijderd");
79
+ setWachtend(null);
80
+ return { ok: true };
87
81
  }
88
82
  catch (e) {
89
- toast.error(`Verwijderen mislukt: ${e.message}`);
90
- }
91
- finally {
92
- setBusy(false);
83
+ if (pad)
84
+ await supabase.storage.from(BUCKET).remove([pad]);
85
+ return { ok: false, melding: `Traysticker: opslaan mislukt (${e.message}).` };
93
86
  }
94
87
  };
95
- return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Traysticker-afbeelding" }), _jsx(CardDescription, { children: "Afbeelding die op het traysticker-etiket bij de pakbon op het pickstation wordt geprint. Dit staat los van de productfoto uit Floriday." })] }), _jsxs(CardContent, { className: "space-y-4", children: [loading ? (_jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })) : row ? (_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: row.bestand_url, alt: "Traysticker", className: "h-24 w-24 rounded border object-contain bg-muted" }), _jsxs("div", { className: "text-sm text-muted-foreground", children: [_jsx("div", { children: row.bestandsnaam }), row.bestandsgrootte != null && _jsxs("div", { children: [Math.round(row.bestandsgrootte / 1024), " KB"] })] })] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen traysticker-afbeelding ge\u00FCpload." })), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
88
+ useOpslagDeelnemer("traysticker", wachtend !== null, opslaan);
89
+ const toonUrl = wachtend?.soort === "nieuw" ? wachtend.voorbeeld : wachtend?.soort === "weg" ? null : row?.bestand_url;
90
+ const toonNaam = wachtend?.soort === "nieuw" ? wachtend.file.name : row?.bestandsnaam;
91
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Traysticker-afbeelding" }), _jsx(CardDescription, { children: "Afbeelding die op het traysticker-etiket bij de pakbon op het pickstation wordt geprint. Dit staat los van de productfoto uit Floriday." })] }), _jsxs(CardContent, { className: "space-y-4", children: [loading ? (_jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })) : toonUrl ? (_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: toonUrl, alt: "Traysticker", className: "h-24 w-24 rounded border object-contain bg-muted" }), _jsx("div", { className: "text-sm text-muted-foreground", children: _jsx("div", { children: toonNaam }) })] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: wachtend?.soort === "weg" ? "Wordt verwijderd bij Opslaan." : "Nog geen traysticker-afbeelding." })), wachtend && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Niet opgeslagen wijziging; bewaar met \"Opslaan\" bovenaan." })), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
96
92
  const f = e.target.files?.[0];
97
93
  if (f)
98
- void upload(f);
99
- } }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { onClick: () => inputRef.current?.click(), disabled: busy || loading, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(Upload, { className: "mr-2 h-4 w-4" }), row ? "Vervangen" : "Afbeelding uploaden"] }), row && (_jsxs(Button, { variant: "outline", onClick: remove, disabled: busy, children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] }))] })] })] }));
94
+ setWachtend({ soort: "nieuw", file: f, voorbeeld: URL.createObjectURL(f) });
95
+ if (inputRef.current)
96
+ inputRef.current.value = "";
97
+ } }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => inputRef.current?.click(), disabled: loading, children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), row || wachtend?.soort === "nieuw" ? "Andere kiezen" : "Afbeelding kiezen"] }), row && wachtend?.soort !== "weg" && (_jsxs(Button, { variant: "outline", onClick: () => setWachtend({ soort: "weg" }), children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] })), wachtend && (_jsxs(Button, { variant: "ghost", onClick: () => setWachtend(null), children: [_jsx(Undo2, { className: "mr-2 h-4 w-4" }), "Ongedaan maken"] }))] })] })] }));
100
98
  }
@@ -1,9 +1,4 @@
1
1
  import type { TradeItem } from "../../lib/floriday-client.js";
2
- /**
3
- * Overzicht van producten die (nog) niet actief op Floriday staan. Er is geen concept-status
4
- * meer — het gaat puur om welke Floriday-velden ontbreken en of het product klaar is om te
5
- * activeren. Producten blijven volwaardig in gebruik binnen ERP, ongeacht Floriday-status.
6
- */
7
2
  export declare function ValidationOverviewTable({ items, connectionId, onChanged, }: {
8
3
  items: TradeItem[];
9
4
  connectionId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"ValidationOverviewTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ValidationOverviewTable.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAQ3D;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,YAAY,EACZ,SAAS,GACV,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,+BAwKA"}
1
+ {"version":3,"file":"ValidationOverviewTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ValidationOverviewTable.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoB3D,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,YAAY,EACZ,SAAS,GACV,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,+BAwKA"}
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
3
3
  import { useNavigate } from "@tanstack/react-router";
4
4
  import { Badge, Button } from "@flowselections/core";
5
5
  import { Link2, Loader2, Pencil, Send } from "lucide-react";
6
- import { validateDraftForPublish } from "../../lib/floriday-import-validator.js";
6
+ import { conceptMetArtikel, validateDraftForPublish } from "../../lib/floriday-import-validator.js";
7
7
  import { updateDraftMeta } from "../../hooks/useLocalTradeItems.js";
8
8
  import { floriday } from "../../lib/floriday-client.js";
9
9
  import { relinkProducts } from "../../lib/floriday-relink.js";
@@ -13,6 +13,12 @@ import { toast } from "@flowselections/core";
13
13
  * meer — het gaat puur om welke Floriday-velden ontbreken en of het product klaar is om te
14
14
  * activeren. Producten blijven volwaardig in gebruik binnen ERP, ongeacht Floriday-status.
15
15
  */
16
+ /** Eigen naam en artikelnummer van het artikel (niet het Floriday-concept). */
17
+ function artikelVan(it) {
18
+ const raw = (it.raw ?? {});
19
+ const s = (v) => (typeof v === "string" && v.trim() ? v : null);
20
+ return { name: s(raw.product) ?? s(it.name), articleNumber: s(raw.article_number) ?? s(it.articleNumber) };
21
+ }
16
22
  export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
17
23
  const navigate = useNavigate();
18
24
  const [relinking, setRelinking] = useState(false);
@@ -53,14 +59,14 @@ export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
53
59
  item.raw?.floriday_payload ??
54
60
  item.raw?.data ??
55
61
  {};
56
- const v = validateDraftForPublish(payload);
62
+ const v = validateDraftForPublish(conceptMetArtikel(payload, artikelVan(item)));
57
63
  if (!v.ok) {
58
64
  toast.error("Nog niet compleet: " + v.missing.join(", "));
59
65
  return;
60
66
  }
61
67
  try {
62
68
  const { buildAddTradeItemPayload } = await import("../../lib/floriday-payload.js");
63
- const built = buildAddTradeItemPayload(payload);
69
+ const built = buildAddTradeItemPayload(conceptMetArtikel(payload, artikelVan(item)));
64
70
  const res = await floriday.createTradeItem(connectionId, built);
65
71
  await updateDraftMeta(productId, {
66
72
  floridayLinkStatus: "published",
@@ -85,7 +91,7 @@ export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
85
91
  it.raw?.floriday_payload ??
86
92
  it.raw?.data ??
87
93
  {};
88
- const v = validateDraftForPublish(payload);
94
+ const v = validateDraftForPublish(conceptMetArtikel(payload, artikelVan(it)));
89
95
  return (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "p-3 font-mono text-xs", children: it.articleNumber || "—" }), _jsx("td", { className: "p-3", children: it.name }), _jsx("td", { className: "p-3", children: _jsx(Badge, { className: "bg-slate-500/15 text-slate-700 border-slate-500/30", children: "Niet actief op Floriday" }) }), _jsx("td", { className: "p-3 text-xs text-muted-foreground max-w-[260px]", children: reasonFor(it) }), _jsx("td", { className: "p-3", children: v.missing.length === 0 ? (_jsx("span", { className: "text-xs text-emerald-700", children: "Klaar om te activeren" })) : (_jsxs("span", { className: "text-xs text-muted-foreground", title: v.missing.join(", "), children: ["Nog nodig: ", v.missing.slice(0, 3).join(", ").toLowerCase(), v.missing.length > 3 ? ` en nog ${v.missing.length - 3} andere` : "", "."] })) }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
90
96
  const target = it.productId ?? it.localId ?? it.id;
91
97
  navigate({ to: "/voorraad/$id", params: { id: target } });
@@ -0,0 +1,3 @@
1
+ /** Inklapbaar blok "Beheer": alleen zichtbaar voor beheerders, alleen informatie plus één knop. */
2
+ export declare function VoorraadBeheerCard(): import("react").JSX.Element | null;
3
+ //# sourceMappingURL=VoorraadBeheerCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoorraadBeheerCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadBeheerCard.tsx"],"names":[],"mappings":"AAuBA,mGAAmG;AACnG,wBAAgB,kBAAkB,uCAwDjC"}
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { Button, Card, CardContent, CardHeader, CardTitle, toast } from "@flowselections/core";
4
+ import { supabase } from "../../integrations/supabase/client.js";
5
+ import { dbFoutTekst } from "../../lib/db-error-message.js";
6
+ import { toonVersieBeheer } from "../../lib/floriday-versie-ronde.js";
7
+ import { floridayVersiesControleren } from "../../lib/floriday-versie-ronde.functions.js";
8
+ const STATUS_TEKST = {
9
+ geslaagd: "geslaagd",
10
+ onvolledig: "nog niet compleet (wordt automatisch opnieuw geprobeerd)",
11
+ mislukt: "mislukt (wordt automatisch opnieuw geprobeerd)",
12
+ };
13
+ /** Inklapbaar blok "Beheer": alleen zichtbaar voor beheerders, alleen informatie plus één knop. */
14
+ export function VoorraadBeheerCard() {
15
+ const [status, setStatus] = useState(null);
16
+ const [bezig, setBezig] = useState(false);
17
+ const laad = useCallback(async () => {
18
+ const { data, error } = await supabase.rpc("voorraad_floriday_versieronde_status");
19
+ if (error) {
20
+ setStatus(null);
21
+ return;
22
+ }
23
+ setStatus(data);
24
+ }, []);
25
+ useEffect(() => { void laad(); }, [laad]);
26
+ if (!toonVersieBeheer(status) || !status?.heeft_koppeling)
27
+ return null;
28
+ const start = async () => {
29
+ setBezig(true);
30
+ try {
31
+ await floridayVersiesControleren();
32
+ toast.success("Controle gestart. Dit loopt op de achtergrond.");
33
+ await laad();
34
+ }
35
+ catch (e) {
36
+ toast.error(dbFoutTekst(e));
37
+ }
38
+ finally {
39
+ setBezig(false);
40
+ }
41
+ };
42
+ const l = status.laatste;
43
+ return (_jsx(Card, { children: _jsxs("details", { children: [_jsx("summary", { className: "cursor-pointer list-none", children: _jsx(CardHeader, { children: _jsx(CardTitle, { children: "Beheer" }) }) }), _jsxs(CardContent, { className: "space-y-2 text-sm", children: [_jsx("p", { className: "font-medium", children: "Controle van Floriday-artikelen" }), _jsx("p", { className: "text-muted-foreground", children: "Loopt vanzelf elke nacht. Oude versies van Floriday-artikelen worden geen zichtbaar artikel." }), _jsxs("p", { children: ["Laatste controle:", " ", l ? `${new Date(l.uitgevoerd_at).toLocaleString("nl-NL")} — ${STATUS_TEKST[l.status] ?? l.status}` : "nog niet uitgevoerd", status.bezig && " · nu bezig"] }), l?.melding && l.status !== "geslaagd" && _jsx("p", { className: "text-muted-foreground", children: l.melding }), _jsxs("p", { children: ["Vervangen in Floriday: ", status.vervangen ?? 0, " \u00B7 Vervallen: ", status.vervallen ?? 0, " \u00B7 Apart gezet (geen antwoord van Floriday): ", status.onbekend ?? 0] }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void start(), disabled: bezig || status.bezig, children: bezig ? "Bezig…" : "Nu opnieuw controleren" })] })] }) }));
44
+ }
@@ -1,15 +1,3 @@
1
- import type { TradeItem } from "../../lib/floriday-client.js";
2
- /**
3
- * "Op voorraad" = artikelen met liggende voorraad (fysiek > 0).
4
- * "Vrij beschikbaar" = artikelen waarvan liggend min gereserveerd > 0.
5
- */
6
- export declare function countStock(items: Array<Pick<TradeItem, "quantity" | "availableQuantity" | "minQuantity">>): {
7
- total: number;
8
- inStock: number;
9
- outOfStock: number;
10
- belowMin: number;
11
- fullyReserved: number;
12
- freeAvailable: number;
13
- };
1
+ export { countStock } from "../../lib/count-stock.js";
14
2
  export declare function VoorraadPage(): import("react").JSX.Element;
15
3
  //# sourceMappingURL=VoorraadPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoF3D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,mBAAmB,GAAG,aAAa,CAAC,CAAC;;;;;;;EAgBzG;AAED,wBAAgB,YAAY,gCAuW3B"}
1
+ {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAwCA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AA2CnD,wBAAgB,YAAY,gCA6M3B"}