@flowselections/floriday-voorraad 1.0.41 → 1.0.43

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 (53) hide show
  1. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/ArtikelWizard.js +15 -11
  3. package/dist-lib/components/voorraad/OrderHistoryCard.js +2 -2
  4. package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts +7 -0
  5. package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts.map +1 -0
  6. package/dist-lib/components/voorraad/StockMutationHistoryCard.js +134 -0
  7. package/dist-lib/components/voorraad/StockPushMonitor.d.ts.map +1 -1
  8. package/dist-lib/components/voorraad/StockPushMonitor.js +90 -12
  9. package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
  10. package/dist-lib/components/voorraad/VoorraadPage.js +1 -1
  11. package/dist-lib/hooks/useFloriday.d.ts.map +1 -1
  12. package/dist-lib/hooks/useFloriday.js +5 -2
  13. package/dist-lib/hooks/useStockPushJobs.d.ts +2 -0
  14. package/dist-lib/hooks/useStockPushJobs.d.ts.map +1 -1
  15. package/dist-lib/hooks/useStockPushJobs.js +18 -5
  16. package/dist-lib/integrations/supabase/auth-middleware.d.ts +1001 -15
  17. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  18. package/dist-lib/integrations/supabase/client.d.ts +1001 -15
  19. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  20. package/dist-lib/integrations/supabase/client.server.d.ts +1001 -15
  21. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  22. package/dist-lib/integrations/supabase/types.d.ts +1001 -10
  23. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  24. package/dist-lib/lib/floricode-required-features.functions.d.ts +1001 -15
  25. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  26. package/dist-lib/lib/floriday-2026v1.server.d.ts +1 -1
  27. package/dist-lib/lib/floriday-2026v1.server.d.ts.map +1 -1
  28. package/dist-lib/lib/floriday-2026v1.server.js +10 -0
  29. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +1001 -15
  30. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/floriday-env.functions.d.ts +1972 -0
  32. package/dist-lib/lib/floriday-env.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/floriday-gateway.functions.d.ts +5916 -0
  34. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  35. package/dist-lib/lib/floriday-gateway.server.d.ts +7 -1
  36. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  37. package/dist-lib/lib/floriday-gateway.server.js +50 -3
  38. package/dist-lib/lib/floriday-vbn-required.functions.d.ts +1972 -0
  39. package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/floriday-warehouse.functions.d.ts +2958 -0
  41. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  42. package/dist-lib/lib/floriday-write-gate.server.d.ts +60 -0
  43. package/dist-lib/lib/floriday-write-gate.server.d.ts.map +1 -0
  44. package/dist-lib/lib/floriday-write-gate.server.js +196 -0
  45. package/dist-lib/lib/floriday-writes.functions.d.ts +1972 -0
  46. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/inventory-settings.functions.d.ts +1972 -0
  48. package/dist-lib/lib/inventory-settings.functions.d.ts.map +1 -1
  49. package/dist-lib/lib/module-health.functions.d.ts +1092 -0
  50. package/dist-lib/lib/module-health.functions.d.ts.map +1 -1
  51. package/dist-lib/lib/stock-sync.functions.d.ts +1972 -0
  52. package/dist-lib/lib/stock-sync.functions.d.ts.map +1 -1
  53. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AAqMA,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,2CAs9DL"}
1
+ {"version":3,"file":"ArtikelWizard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ArtikelWizard.tsx"],"names":[],"mappings":"AAsMA,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,2CA89DL"}
@@ -34,6 +34,7 @@ import { VoorraadStockCard } from "../../components/voorraad/VoorraadStockCard";
34
34
  import { ProductLinkCard } from "../../components/voorraad/ProductLinkCard";
35
35
  import { PriceStatusBadge } from "../../components/voorraad/PriceStatusBadge";
36
36
  import { OrderHistoryCard } from "../../components/voorraad/OrderHistoryCard";
37
+ import { StockMutationHistoryCard } from "../../components/voorraad/StockMutationHistoryCard";
37
38
  import { ArtikelCompositionCard } from "../../components/voorraad/ArtikelCompositionCard";
38
39
  import { StepSamenstelling } from "../../components/voorraad/StepSamenstelling";
39
40
  import { FloridayInfoCard } from "../../components/voorraad/FloridayInfoCard";
@@ -139,7 +140,10 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
139
140
  // van elke geleerde eis, en de gebruiker nooit dezelfde 400 twee keer krijgt.
140
141
  const queryClient = useQueryClient();
141
142
  const isEditingDraft = Boolean(initialDraftId);
142
- const isEditingPublished = Boolean(tradeItemId);
143
+ // Een lokaal product dat al aan Floriday gekoppeld is, is géén "nieuw"
144
+ // artikel: bewerken betekent dan bijwerken in Floriday.
145
+ const publishedTradeItemId = tradeItemId ?? productMeta.floridayTradeItemId ?? null;
146
+ const isEditingPublished = Boolean(publishedTradeItemId);
143
147
  const isEditingProduct = Boolean(productId);
144
148
  const isEditing = isEditingDraft || isEditingPublished || isEditingProduct;
145
149
  const lastSavedRef = useRef(null);
@@ -969,7 +973,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
969
973
  id: draftId,
970
974
  connectionId: activeId,
971
975
  data,
972
- floridayTradeItemId: (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null),
976
+ floridayTradeItemId: (publishedTradeItemId || draftFloridayId || null),
973
977
  sync: {
974
978
  dirty: true,
975
979
  last_attempt_at: new Date().toISOString(),
@@ -1038,7 +1042,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1038
1042
  id: draftId,
1039
1043
  connectionId: activeId,
1040
1044
  data,
1041
- floridayTradeItemId: (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null),
1045
+ floridayTradeItemId: (publishedTradeItemId || draftFloridayId || null),
1042
1046
  sync: { dirty: true, last_error: null },
1043
1047
  });
1044
1048
  if (localId && localId !== draftId)
@@ -1104,18 +1108,18 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1104
1108
  }
1105
1109
  setPublishJobId(jobId);
1106
1110
  const stepOpts = { characteristicLabels };
1107
- let resolvedTradeItemId = tradeItemId ?? null;
1111
+ let resolvedTradeItemId = publishedTradeItemId ?? null;
1108
1112
  try {
1109
1113
  // 1) Productgegevens -------------------------------------------------
1110
1114
  const core = await runPublishStep(jobId, "trade-item", async () => {
1111
- if (isEditingPublished && tradeItemId) {
1112
- await saveWarehousePreference(tradeItemId, data.warehouseId);
1115
+ if (publishedTradeItemId) {
1116
+ await saveWarehousePreference(publishedTradeItemId, data.warehouseId);
1113
1117
  const payload = buildUpdateTradeItemPayload(data);
1114
- const res = await floriday.updateTradeItem(activeId, tradeItemId, payload);
1118
+ const res = await floriday.updateTradeItem(activeId, publishedTradeItemId, payload);
1115
1119
  if (!res?.attemptId) {
1116
1120
  throw new Error("De wijziging is niet bevestigd door de handelsomgeving");
1117
1121
  }
1118
- return { tradeItemId };
1122
+ return { tradeItemId: publishedTradeItemId };
1119
1123
  }
1120
1124
  const payload = buildAddTradeItemPayload(data);
1121
1125
  const res = await floriday.createTradeItem(activeId, payload);
@@ -1374,7 +1378,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1374
1378
  const persistAndPush = useCallback(async (snapshot) => {
1375
1379
  if (!activeId)
1376
1380
  return false;
1377
- const pushTargetId = (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null);
1381
+ const pushTargetId = (publishedTradeItemId || draftFloridayId || null);
1378
1382
  setSyncState("saving");
1379
1383
  setSyncError(null);
1380
1384
  // 1. Lokaal opslaan — nooit overslaan.
@@ -1479,7 +1483,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1479
1483
  return;
1480
1484
  if (draftLoading || publishedLoading || productLoading)
1481
1485
  return;
1482
- const pushTargetId = (isEditingPublished && tradeItemId) ? tradeItemId : (draftFloridayId || null);
1486
+ const pushTargetId = (publishedTradeItemId || draftFloridayId || null);
1483
1487
  if (!draftId && !pushTargetId)
1484
1488
  return;
1485
1489
  const serialized = JSON.stringify(data);
@@ -1545,7 +1549,7 @@ export function ArtikelWizard({ draftId: initialDraftId, tradeItemId, productId,
1545
1549
  name: data.name || data.vbnName,
1546
1550
  articleNumber: data.articleCode,
1547
1551
  gtin: data.gtin,
1548
- } }), _jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: linkedProductId }), _jsx(OrderHistoryCard, { tradeItemKey: localTradeItemKey, productName: data.name || data.vbnName || null, productId: linkedProductId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: localTradeItemKey, productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
1552
+ } }), _jsx(VoorraadStockCard, { connectionId: activeId, tradeItemId: tradeItemId ?? null, draftId: draftId ?? null, productId: linkedProductId }), _jsx(OrderHistoryCard, { tradeItemKey: localTradeItemKey, productName: data.name || data.vbnName || null, productId: linkedProductId ?? null }), _jsx(StockMutationHistoryCard, { tradeItemKey: localTradeItemKey, productId: linkedProductId ?? null }), _jsx(ProductLinkCard, { tradeItemKey: localTradeItemKey, productName: data.vbnName || null })] })), _jsx("ol", { className: "space-y-3", children: steps.map((s, i) => {
1549
1553
  const isOpen = i === step;
1550
1554
  const isValid = stepValidity[i];
1551
1555
  const isVisited = i < step || isEditing;
@@ -123,11 +123,11 @@ export function OrderHistoryCard({ tradeItemKey, productName, productId }) {
123
123
  supabase.removeChannel(channel);
124
124
  };
125
125
  }, [tradeItemKey, productName, productId]);
126
- 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(ClipboardList, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Bestelgeschiedenis" }), _jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: rows ? `${rows.length} bestelling${rows.length === 1 ? "" : "en"}` : "" })] }), _jsxs(CardContent, { className: "pt-0", children: [error && (_jsxs("div", { className: "text-sm text-destructive", children: ["Kon bestelgeschiedenis niet laden: ", 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" }), " Bestellingen laden\u2026"] })), rows && rows.length === 0 && !error && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen bestellingen voor dit artikel." })), rows && rows.length > 0 && (_jsx("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: "Ordernummer" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Klant" }), _jsx("th", { className: "px-3 py-2 text-right", children: "Fusten" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Besteldatum" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Status" }), _jsx("th", { className: "px-3 py-2" })] }) }), _jsx("tbody", { children: rows.map((r) => {
126
+ 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(ClipboardList, { className: "h-4 w-4" }) }), _jsx("span", { className: "font-medium", children: "Bestelgeschiedenis" }), _jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: rows ? `${rows.length} bestelling${rows.length === 1 ? "" : "en"}` : "" })] }), _jsxs(CardContent, { className: "pt-0", children: [error && (_jsxs("div", { className: "text-sm text-destructive", children: ["Kon bestelgeschiedenis niet laden: ", 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" }), " Bestellingen laden\u2026"] })), rows && rows.length === 0 && !error && (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen bestellingen voor dit artikel." })), rows && rows.length > 0 && (_jsx("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: "Ordernummer" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Klant" }), _jsx("th", { className: "px-3 py-2 text-right", children: "Fusten" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Besteldatum" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Status" }), _jsx("th", { className: "px-3 py-2" })] }) }), _jsx("tbody", { children: rows.map((r, i) => {
127
127
  const label = normalizeStatus(r.status);
128
128
  const tone = STATUS_TONE[label.toLowerCase()] ??
129
129
  "bg-slate-100 text-slate-700 border-slate-200";
130
130
  const href = `/floriday-verkoop/verkooporders/${r.order_id}`;
131
- return (_jsxs("tr", { className: "border-t hover:bg-muted/40 cursor-pointer", onClick: () => { window.location.href = href; }, children: [_jsx("td", { className: "px-3 py-2 font-medium", children: _jsx("a", { href: href, className: "text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: r.order_number }) }), _jsx("td", { className: "px-3 py-2", children: r.customer_name ?? "—" }), _jsxs("td", { className: "px-3 py-2 text-right tabular-nums", children: [r.quantity, r.unit ? _jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: r.unit }) : null] }), _jsx("td", { className: "px-3 py-2", children: formatDate(r.order_date ?? r.created_at) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: `border ${tone}`, children: label }) }), _jsx("td", { className: "px-3 py-2 text-right text-muted-foreground", children: _jsx(ExternalLink, { className: "h-3.5 w-3.5 inline" }) })] }, r.order_id));
131
+ return (_jsxs("tr", { className: "border-t hover:bg-muted/40 cursor-pointer", onClick: () => { window.location.href = href; }, children: [_jsx("td", { className: "px-3 py-2 font-medium", children: _jsx("a", { href: href, className: "text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: r.order_number }) }), _jsx("td", { className: "px-3 py-2", children: r.customer_name ?? "—" }), _jsxs("td", { className: "px-3 py-2 text-right tabular-nums", children: [r.quantity, r.unit ? _jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: r.unit }) : null] }), _jsx("td", { className: "px-3 py-2", children: formatDate(r.order_date ?? r.created_at) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: `border ${tone}`, children: label }) }), _jsx("td", { className: "px-3 py-2 text-right text-muted-foreground", children: _jsx(ExternalLink, { className: "h-3.5 w-3.5 inline" }) })] }, `${r.order_id}-${i}`));
132
132
  }) })] }) }))] })] }));
133
133
  }
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ tradeItemKey: string | null;
3
+ productId?: string | null;
4
+ }
5
+ export declare function StockMutationHistoryCard({ tradeItemKey, productId }: Props): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=StockMutationHistoryCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StockMutationHistoryCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockMutationHistoryCard.tsx"],"names":[],"mappings":"AAUA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AA8CD,wBAAgB,wBAAwB,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,KAAK,2CAiM1E"}
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Afboekgeschiedenis: wanneer is er voorraad bij- of afgeboekt en waarom.
4
+ * Leest public.stock_mutation_log op de sleutel van dit artikel (inclusief
5
+ * de groepssleutel wanneer het artikel via een groep wordt afgeboekt).
6
+ */
7
+ import { useEffect, useState } from "react";
8
+ import { Card, CardContent, Badge } from "@flowselections/core";
9
+ import { Loader2, History } from "lucide-react";
10
+ import { supabase } from "../../integrations/supabase/client";
11
+ const TYPE_LABEL = {
12
+ consumption: { label: "Afboeking (verkoop)", tone: "bg-rose-100 text-rose-800 border-rose-200" },
13
+ consumption_blocked: { label: "Afboeking geblokkeerd", tone: "bg-amber-100 text-amber-800 border-amber-200" },
14
+ consumption_skipped: { label: "Afboeking overgeslagen", tone: "bg-amber-100 text-amber-800 border-amber-200" },
15
+ reservation: { label: "Reservering", tone: "bg-blue-100 text-blue-800 border-blue-200" },
16
+ reservation_release: { label: "Reservering vrijgegeven", tone: "bg-blue-100 text-blue-800 border-blue-200" },
17
+ reservation_skipped: { label: "Reservering overgeslagen", tone: "bg-amber-100 text-amber-800 border-amber-200" },
18
+ manual_correction: { label: "Handmatige correctie", tone: "bg-slate-100 text-slate-700 border-slate-200" },
19
+ order_sync: { label: "Verwerking verkooporders", tone: "bg-indigo-100 text-indigo-800 border-indigo-200" },
20
+ import: { label: "Import", tone: "bg-slate-100 text-slate-700 border-slate-200" },
21
+ production: { label: "Productie", tone: "bg-emerald-100 text-emerald-800 border-emerald-200" },
22
+ stock_update: { label: "Voorraad bijgewerkt", tone: "bg-slate-100 text-slate-700 border-slate-200" },
23
+ };
24
+ function formatMoment(v) {
25
+ try {
26
+ const d = new Date(v);
27
+ if (Number.isNaN(d.getTime()))
28
+ return v;
29
+ return d.toLocaleString("nl-NL", {
30
+ day: "2-digit",
31
+ month: "short",
32
+ year: "numeric",
33
+ hour: "2-digit",
34
+ minute: "2-digit",
35
+ });
36
+ }
37
+ catch {
38
+ return v;
39
+ }
40
+ }
41
+ export function StockMutationHistoryCard({ tradeItemKey, productId }) {
42
+ const [rows, setRows] = useState(null);
43
+ const [error, setError] = useState(null);
44
+ useEffect(() => {
45
+ let cancelled = false;
46
+ const load = async () => {
47
+ setError(null);
48
+ if (!tradeItemKey && !productId) {
49
+ setRows([]);
50
+ return;
51
+ }
52
+ try {
53
+ const keys = new Set();
54
+ if (tradeItemKey)
55
+ keys.add(tradeItemKey);
56
+ if (productId)
57
+ keys.add(productId);
58
+ // Groepssleutels waaronder dit artikel ook afgeboekt kan worden.
59
+ const { data: groups } = await supabase
60
+ .from("product_group_items")
61
+ .select("product_group_id")
62
+ .in("trade_item_id", [...keys])
63
+ .limit(20);
64
+ for (const g of (groups ?? [])) {
65
+ keys.add(`group:${g.product_group_id}`);
66
+ }
67
+ const { data, error } = await supabase
68
+ .from("stock_mutation_log")
69
+ .select("id,created_at,mutation_type,qty_delta,physical_before,physical_after,reserved_before,reserved_after,reason,order_id")
70
+ .in("trade_item_key", [...keys])
71
+ .order("created_at", { ascending: false })
72
+ .limit(100);
73
+ if (error)
74
+ throw error;
75
+ if (cancelled)
76
+ return;
77
+ // Ordernummers los ophalen: er is geen databaserelatie tussen de
78
+ // afboekingen en de orders, dus een gecombineerde vraag mislukt.
79
+ const orderIds = Array.from(new Set((data ?? []).map((r) => r.order_id).filter(Boolean)));
80
+ const orderNumbers = new Map();
81
+ if (orderIds.length > 0) {
82
+ const { data: orderRows } = await supabase
83
+ .from("orders")
84
+ .select("id, order_number")
85
+ .in("id", orderIds);
86
+ for (const o of (orderRows ?? [])) {
87
+ orderNumbers.set(String(o.id), String(o.order_number ?? ""));
88
+ }
89
+ }
90
+ if (cancelled)
91
+ return;
92
+ setRows((data ?? []).map((r) => ({
93
+ id: r.id,
94
+ created_at: r.created_at,
95
+ mutation_type: r.mutation_type ?? "",
96
+ qty_delta: Number(r.qty_delta ?? 0),
97
+ physical_before: r.physical_before === null ? null : Number(r.physical_before),
98
+ physical_after: r.physical_after === null ? null : Number(r.physical_after),
99
+ reserved_before: r.reserved_before === null ? null : Number(r.reserved_before),
100
+ reserved_after: r.reserved_after === null ? null : Number(r.reserved_after),
101
+ reason: r.reason ?? null,
102
+ order_id: r.order_id ?? null,
103
+ order_number: r.order_id ? orderNumbers.get(String(r.order_id)) ?? null : null,
104
+ })));
105
+ }
106
+ catch (e) {
107
+ if (!cancelled)
108
+ setError(e.message);
109
+ }
110
+ };
111
+ load();
112
+ const channel = supabase
113
+ .channel(`stock-mutations-${tradeItemKey ?? productId ?? "none"}`)
114
+ .on("postgres_changes", { event: "*", schema: "public", table: "stock_mutation_log" }, load)
115
+ .subscribe();
116
+ return () => {
117
+ cancelled = true;
118
+ supabase.removeChannel(channel);
119
+ };
120
+ }, [tradeItemKey, productId]);
121
+ 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 && (_jsxs("div", { className: "text-sm text-destructive", children: ["Kon voorraadmutaties niet laden: ", 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 && (_jsx("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: "Reden" })] }) }), _jsx("tbody", { children: rows.map((r) => {
122
+ const meta = TYPE_LABEL[r.mutation_type] ?? {
123
+ label: r.mutation_type || "Onbekend",
124
+ tone: "bg-slate-100 text-slate-700 border-slate-200",
125
+ };
126
+ const physChanged = r.physical_before !== null &&
127
+ r.physical_after !== null &&
128
+ r.physical_before !== r.physical_after;
129
+ const resChanged = r.reserved_before !== null &&
130
+ r.reserved_after !== null &&
131
+ r.reserved_before !== r.reserved_after;
132
+ return (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "whitespace-nowrap px-3 py-2", children: formatMoment(r.created_at) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: `border ${meta.tone}`, children: meta.label }) }), _jsxs("td", { className: `px-3 py-2 text-right tabular-nums font-medium ${r.qty_delta < 0 ? "text-rose-700" : r.qty_delta > 0 ? "text-emerald-700" : ""}`, children: [r.qty_delta > 0 ? "+" : "", r.qty_delta] }), _jsxs("td", { className: "whitespace-nowrap px-3 py-2 tabular-nums text-muted-foreground", children: [physChanged && (_jsxs("div", { children: ["Liggend ", r.physical_before, " \u2192 ", r.physical_after] })), resChanged && (_jsxs("div", { children: ["Gereserveerd ", r.reserved_before, " \u2192 ", r.reserved_after] })), !physChanged && !resChanged && _jsx("span", { children: "\u2014" })] }), _jsx("td", { className: "whitespace-nowrap px-3 py-2", children: r.order_id ? (_jsx("a", { href: `/floriday-verkoop/verkooporders/${r.order_id}`, className: "text-primary hover:underline", children: r.order_number ?? "Order" })) : (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })) }), _jsx("td", { className: "px-3 py-2 text-muted-foreground", children: r.reason ?? "—" })] }, r.id));
133
+ }) })] }) }))] })] }));
134
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"StockPushMonitor.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockPushMonitor.tsx"],"names":[],"mappings":"AAiFA,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,2CAyJjF"}
1
+ {"version":3,"file":"StockPushMonitor.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/StockPushMonitor.tsx"],"names":[],"mappings":"AAqKA,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,2CA+LjF"}
@@ -12,26 +12,96 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
12
12
  import { useServerFn } from "@tanstack/react-start";
13
13
  import { toast } from "sonner";
14
14
  import { ChevronDown, ChevronRight, RefreshCw, Send } from "lucide-react";
15
+ import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
15
16
  import { supabase } from "../../integrations/supabase/client";
16
17
  import { useStockPushJobs, useCentralAuthBlockade } from "../../hooks/useStockPushJobs";
17
18
  import { enqueueTestStockPush } from "../../lib/stock-sync.functions";
18
19
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
+ /** Een verwerker houdt een regel maximaal 5 minuten vast. */
21
+ const LOCK_STALE_MS = 5 * 60000;
19
22
  const STATUS_LABEL = {
20
23
  pending: "In wachtrij",
21
24
  processing: "Bezig",
25
+ sent: "Verstuurd",
22
26
  completed: "Verstuurd",
23
27
  failed: "Mislukt",
28
+ dead_letter: "Definitief mislukt",
24
29
  superseded: "Vervangen",
25
30
  };
26
- function statusClass(status) {
27
- if (status === "completed")
31
+ function toneClass(tone) {
32
+ if (tone === "ok")
28
33
  return "bg-emerald-500/15 text-emerald-700";
29
- if (status === "failed")
34
+ if (tone === "error")
30
35
  return "bg-destructive/15 text-destructive";
31
- if (status === "superseded")
36
+ if (tone === "muted")
32
37
  return "bg-muted text-muted-foreground";
33
38
  return "bg-amber-500/15 text-amber-700";
34
39
  }
40
+ /** Namen komen soms als taalobject binnen ({ nl: "…" }). */
41
+ function plainName(value) {
42
+ if (typeof value === "string")
43
+ return value.trim() || null;
44
+ if (value && typeof value === "object") {
45
+ const first = value.nl ?? value.en ?? Object.values(value)[0];
46
+ if (typeof first === "string")
47
+ return first.trim() || null;
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Vertaalt de technische foutregel naar wat er aan de hand is en wat er moet
53
+ * gebeuren. Bewust concreet: een tokenfout lost zichzelf nooit op.
54
+ */
55
+ function explainError(raw) {
56
+ if (!raw)
57
+ return null;
58
+ const m = raw.toLowerCase();
59
+ if (m.includes("token") || m.includes("requested path is invalid")) {
60
+ return {
61
+ text: "De Floriday-Authenticatiemodule kon geen geldig token ophalen.",
62
+ action: "Controleer in de Floriday-Authenticatiemodule het adres van Central Auth en of de gepubliceerde versie actueel is. Plan de opdracht daarna opnieuw in.",
63
+ };
64
+ }
65
+ if (m.includes("credential") || m.includes("central auth")) {
66
+ return {
67
+ text: "Er zijn geen actieve Floriday-inloggegevens voor deze omgeving.",
68
+ action: "Zet in Central Auth de juiste inloggegevens voor deze omgeving actief.",
69
+ };
70
+ }
71
+ if (m.includes("prijs")) {
72
+ return { text: raw, action: "Vul een prijs in bij dit artikel en probeer opnieuw." };
73
+ }
74
+ if (m.includes("cache")) {
75
+ return { text: raw, action: "Synchroniseer de Floriday-artikelen en probeer opnieuw." };
76
+ }
77
+ if (m.includes("429") || m.includes("rate limit") || m.includes("snelheidslimiet")) {
78
+ return { text: "Floriday liet even geen nieuwe berichten toe.", action: "Dit gaat vanzelf; de volgende ronde pakt het op." };
79
+ }
80
+ return { text: raw, action: null };
81
+ }
82
+ /** Status zoals de gebruiker hem hoort te zien, inclusief vastgelopen regels. */
83
+ function displayStatus(job) {
84
+ if (job.status === "processing") {
85
+ const started = job.lockedAt ? new Date(job.lockedAt).getTime() : NaN;
86
+ if (!Number.isFinite(started) || Date.now() - started > LOCK_STALE_MS) {
87
+ return {
88
+ label: "Vastgelopen",
89
+ tone: "error",
90
+ hint: "De verwerker heeft deze opdracht opgepakt maar nooit afgerond. Hij wordt automatisch opnieuw opgepakt.",
91
+ };
92
+ }
93
+ return { label: "Bezig", tone: "warn" };
94
+ }
95
+ if (job.status === "sent" || job.status === "completed")
96
+ return { label: "Verstuurd", tone: "ok" };
97
+ if (job.status === "dead_letter")
98
+ return { label: "Definitief mislukt", tone: "error" };
99
+ if (job.status === "failed")
100
+ return { label: "Mislukt", tone: "error" };
101
+ if (job.status === "superseded")
102
+ return { label: "Vervangen", tone: "muted" };
103
+ return { label: STATUS_LABEL[job.status] ?? job.status, tone: "warn" };
104
+ }
35
105
  function timeLabel(iso) {
36
106
  if (!iso)
37
107
  return "—";
@@ -62,9 +132,12 @@ function useLinkedTradeItems(connectionId) {
62
132
  const names = new Map();
63
133
  for (const row of (cached ?? [])) {
64
134
  const d = row.data;
65
- const label = d?.tradeItemName ?? d?.name ?? d?.productName ?? null;
135
+ const label = plainName(d?.tradeItemName) ??
136
+ plainName(d?.name) ??
137
+ plainName(d?.productName) ??
138
+ plainName(d?.vbnProductName);
66
139
  if (label)
67
- names.set(String(row.floriday_id), String(label));
140
+ names.set(String(row.floriday_id), label);
68
141
  }
69
142
  return rows
70
143
  .filter((r) => names.has(String(r.trade_item_key)))
@@ -85,11 +158,11 @@ export function StockPushMonitor({ connectionId }) {
85
158
  const blockade = useCentralAuthBlockade();
86
159
  const items = useLinkedTradeItems(open ? connectionId : null);
87
160
  const testPush = useServerFn(enqueueTestStockPush);
161
+ const itemList = items.data ?? [];
88
162
  useEffect(() => {
89
- if (!selected && items.data && items.data.length > 0) {
90
- setSelected(items.data[0].id);
91
- }
92
- }, [items.data, selected]);
163
+ if (!selected && itemList.length > 0)
164
+ setSelected(itemList[0].id);
165
+ }, [itemList, selected]);
93
166
  const mutation = useMutation({
94
167
  mutationFn: (tradeItemId) => testPush({ data: { tradeItemId } }),
95
168
  onSuccess: (res) => {
@@ -104,7 +177,12 @@ export function StockPushMonitor({ connectionId }) {
104
177
  if (rows.length === 0)
105
178
  return "nog geen pushes";
106
179
  const last = rows[0];
107
- return `laatste: ${STATUS_LABEL[last.status] ?? last.status} om ${timeLabel(last.createdAt)}`;
180
+ return `laatste: ${displayStatus(last).label} om ${timeLabel(last.createdAt)}`;
108
181
  }, [rows]);
109
- return (_jsxs("section", { className: "rounded-lg border bg-card", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium", onClick: () => setOpen((v) => !v), children: [open ? (_jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })), "Voorraadsync naar Floriday testen", _jsx("span", { className: "ml-auto text-xs font-normal text-muted-foreground", children: summary })] }), open && (_jsxs("div", { className: "space-y-4 border-t px-4 py-4 text-sm", children: [_jsx("p", { className: "text-muted-foreground", children: "Elke voorraadwijziging van een gekoppeld artikel zet automatisch een opdracht in de wachtrij. De Floriday-Authenticatiemodule verwerkt die elke minuut en stuurt dan het aantal stuks en de beschikbaarheid naar Floriday. Hieronder plan je een testopdracht in met de huidige voorraadstand, zonder iets te wijzigen." }), blockade.data?.blocked && (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-800", children: [_jsx("span", { className: "font-medium", children: "Let op: " }), "de tokenaanvraag bij Central Auth mislukt op dit moment, dus elke push loopt daarop vast. Dit ligt buiten deze module.", blockade.data.message && (_jsxs("span", { className: "block opacity-80", children: ["Melding: ", blockade.data.message] }))] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("select", { className: "h-9 min-w-[16rem] rounded-md border bg-background px-2 text-sm", value: selected, onChange: (e) => setSelected(e.target.value), disabled: !connectionId || items.isLoading || (items.data ?? []).length === 0, children: (items.data ?? []).length === 0 ? (_jsx("option", { value: "", children: items.isLoading ? "Artikelen laden…" : "Geen gekoppelde artikelen gevonden" })) : ((items.data ?? []).map((i) => (_jsxs("option", { value: i.id, children: [i.name, " \u2014 ", i.stock, " stuks"] }, i.id)))) }), _jsxs("button", { type: "button", className: "inline-flex h-9 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50", disabled: !selected || mutation.isPending, onClick: () => selected && mutation.mutate(selected), children: [_jsx(Send, { className: "mr-2 h-4 w-4" }), "Testpush uitvoeren"] }), _jsxs("button", { type: "button", className: "inline-flex h-9 items-center rounded-md border px-3 text-sm font-medium hover:bg-muted disabled:opacity-50", disabled: jobs.isFetching, onClick: () => void jobs.refetch(), children: [_jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${jobs.isFetching ? "animate-spin" : ""}` }), "Ververs"] })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left text-xs", children: [_jsx("thead", { className: "text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-1 pr-3 font-medium", children: "Artikel" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Stuks" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Status" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Pogingen" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Aangemaakt" }), _jsx("th", { className: "py-1 font-medium", children: "Melding" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 6, className: "py-3 text-muted-foreground", children: "Er staan nog geen push-opdrachten in de wachtrij." }) })), rows.map((r) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "py-1.5 pr-3", children: r.name }), _jsx("td", { className: "py-1.5 pr-3 tabular-nums", children: r.numberOfPieces }), _jsx("td", { className: "py-1.5 pr-3", children: _jsx("span", { className: `rounded px-1.5 py-0.5 ${statusClass(r.status)}`, children: STATUS_LABEL[r.status] ?? r.status }) }), _jsx("td", { className: "py-1.5 pr-3 tabular-nums", children: r.attempts }), _jsx("td", { className: "py-1.5 pr-3 whitespace-nowrap", children: timeLabel(r.createdAt) }), _jsx("td", { className: "py-1.5 max-w-[24rem] truncate text-muted-foreground", title: r.lastError ?? "", children: r.lastError ?? "—" })] }, r.id)))] })] }) })] }))] }));
182
+ const stuckCount = rows.filter((r) => displayStatus(r).label === "Vastgelopen").length;
183
+ return (_jsxs("section", { className: "rounded-lg border bg-card", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium", onClick: () => setOpen((v) => !v), children: [open ? (_jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" })), "Voorraadsync naar Floriday testen", _jsx("span", { className: "ml-auto text-xs font-normal text-muted-foreground", children: summary })] }), open && (_jsxs("div", { className: "space-y-4 border-t px-4 py-4 text-sm", children: [_jsx("p", { className: "text-muted-foreground", children: "Elke voorraadwijziging van een gekoppeld artikel zet automatisch een opdracht in de wachtrij. De Floriday-Authenticatiemodule verwerkt die elke minuut en stuurt dan het aantal stuks en de beschikbaarheid naar Floriday. Hieronder plan je een testopdracht in met de huidige voorraadstand, zonder iets te wijzigen." }), blockade.data?.blocked && (_jsxs("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive", children: [_jsx("span", { className: "font-medium", children: "Actie nodig: " }), "de Floriday-Authenticatiemodule krijgt geen geldig token, dus elke push mislukt meteen. Dit lost zichzelf niet op \u2014 controleer daar het adres van Central Auth en de actieve inloggegevens voor deze omgeving.", blockade.data.message && (_jsxs("span", { className: "block opacity-80", children: ["Melding: ", blockade.data.message] }))] })), stuckCount > 0 && (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-800", children: [stuckCount === 1 ? "Eén opdracht is" : `${stuckCount} opdrachten zijn`, " door de verwerker opgepakt maar nooit afgerond. Ze worden na vijf minuten automatisch opnieuw opgepakt."] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Select, { value: selected, onValueChange: setSelected, disabled: !connectionId || items.isLoading || itemList.length === 0, children: [_jsx(SelectTrigger, { className: "h-9 w-[22rem]", children: _jsx(SelectValue, { placeholder: items.isLoading ? "Artikelen laden…" : "Geen gekoppelde artikelen gevonden" }) }), _jsx(SelectContent, { className: "max-h-72", children: itemList.map((i) => (_jsx(SelectItem, { value: i.id, children: _jsxs("span", { className: "flex w-full items-center justify-between gap-3", children: [_jsx("span", { className: "truncate", children: i.name }), _jsxs("span", { className: "tabular-nums text-muted-foreground", children: [i.stock, " stuks"] })] }) }, i.id))) })] }), _jsxs(Button, { type: "button", disabled: !selected || mutation.isPending, onClick: () => selected && mutation.mutate(selected), children: [_jsx(Send, { className: "mr-2 h-4 w-4" }), "Testpush uitvoeren"] }), _jsxs(Button, { type: "button", variant: "outline", disabled: jobs.isFetching, onClick: () => void jobs.refetch(), children: [_jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${jobs.isFetching ? "animate-spin" : ""}` }), "Ververs"] })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left text-xs", children: [_jsx("thead", { className: "text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-1 pr-3 font-medium", children: "Artikel" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Stuks" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Status" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Pogingen" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Aangemaakt" }), _jsx("th", { className: "py-1 pr-3 font-medium", children: "Bijgewerkt" }), _jsx("th", { className: "py-1 font-medium", children: "Melding" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 7, className: "py-3 text-muted-foreground", children: "Er staan nog geen push-opdrachten in de wachtrij." }) })), rows.map((r) => {
184
+ const status = displayStatus(r);
185
+ const detail = explainError(r.lastError);
186
+ return (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "py-1.5 pr-3", children: r.name }), _jsx("td", { className: "py-1.5 pr-3 tabular-nums", children: r.numberOfPieces }), _jsx("td", { className: "py-1.5 pr-3", children: _jsx("span", { className: `rounded px-1.5 py-0.5 ${toneClass(status.tone)}`, title: status.hint ?? "", children: status.label }) }), _jsx("td", { className: "py-1.5 pr-3 tabular-nums", children: r.attempts }), _jsx("td", { className: "py-1.5 pr-3 whitespace-nowrap", children: timeLabel(r.createdAt) }), _jsx("td", { className: "py-1.5 pr-3 whitespace-nowrap", children: timeLabel(r.updatedAt ?? r.createdAt) }), _jsx("td", { className: "py-1.5 max-w-[26rem] text-muted-foreground", children: detail ? (_jsxs("span", { title: r.lastError ?? "", children: [_jsx("span", { className: "block truncate", children: detail.text }), detail.action && (_jsxs("span", { className: "block text-[11px] text-foreground/70", children: ["Wat te doen: ", detail.action] }))] })) : ("—") })] }, r.id));
187
+ })] })] }) })] }))] }));
110
188
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAqGA,wBAAgB,YAAY,4CAiT3B"}
1
+ {"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAqGA,wBAAgB,YAAY,4CAqT3B"}
@@ -117,7 +117,7 @@ export function VoorraadPage() {
117
117
  { id: "out_of_stock", label: "Niet op voorraad", count: counts.outOfStock },
118
118
  { id: "below_min", label: "Onder minimum", count: counts.belowMin },
119
119
  ];
120
- 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: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, disabled: !activeId, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _jsx(StockSyncStatus, {}), _jsx(StockPushMonitor, { connectionId: activeId ?? null }), connError && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Floriday-connectie laden mislukt: ", connError] })), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock") }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
120
+ 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: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, disabled: !activeId, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _jsx(StockSyncStatus, {}), _jsx(StockPushMonitor, { connectionId: activeId ?? null }), connError && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Floriday-connectie laden mislukt: ", connError] })), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock") }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
121
121
  const active = stockFilter === t.id;
122
122
  return (_jsxs("button", { type: "button", onClick: () => setStockFilter(t.id), className: `relative py-3 px-3 text-sm font-medium whitespace-nowrap transition-colors ${active
123
123
  ? "text-foreground"
@@ -1 +1 @@
1
- {"version":3,"file":"useFloriday.d.ts","sourceRoot":"","sources":["../../src/hooks/useFloriday.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAChB,MAAM,EACN,KAAK,CAAC;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC,CACH,CAAC;CACH;AAoMD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,yEA0BhC;AAqBD,wBAAgB,sBAAsB,0EA0CrC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;QAMnD,OAAO;cACD,MAAM;YACR;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE;UAKvD;AAED,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACxC,KAAK,SAAO;QAOF,OAAO;YACH,MAAM;UACR;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,EAAE;UAKxD;AAED;;;;GAIG;AACH,wBAAgB,uCAAuC,SAStD"}
1
+ {"version":3,"file":"useFloriday.d.ts","sourceRoot":"","sources":["../../src/hooks/useFloriday.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAChB,MAAM,EACN,KAAK,CAAC;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC,CACH,CAAC;CACH;AAoMD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,yEA0BhC;AAqBD,wBAAgB,sBAAsB,0EA2CrC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;QAMnD,OAAO;cACD,MAAM;YACR;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE;UAKvD;AAED,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACxC,KAAK,SAAO;QAOF,OAAO;YACH,MAAM;UACR;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,EAAE;UAKxD;AAED;;;;GAIG;AACH,wBAAgB,uCAAuC,SAStD"}
@@ -245,9 +245,12 @@ export function useTradeItemFormSchema() {
245
245
  throw new Error(res?.error ??
246
246
  "Floriday trade-item schema niet beschikbaar (gateway gaf geen ok=true terug)");
247
247
  }
248
+ // Een lege `vbn-product-group` maakt het schema minder compleet, maar mag
249
+ // bewerken en publiceren niet blokkeren: de wizard valt dan terug op de
250
+ // reeds bekende karakteristieken.
248
251
  const vbnList = res.codeLists?.["vbn-product-group"] ?? [];
249
- if (vbnList.length === 0) {
250
- throw new Error("Floriday schema-call is incompleet: codeLists['vbn-product-group'] ontbreekt. Daardoor kunnen VBN-specifieke karakteristieken niet betrouwbaar worden bepaald.");
252
+ if (vbnList.length === 0 && import.meta.env.DEV) {
253
+ console.warn("[floriday-schema] codeLists['vbn-product-group'] is leeg");
251
254
  }
252
255
  return res;
253
256
  },
@@ -10,6 +10,8 @@ export interface StockPushJob {
10
10
  createdAt: string;
11
11
  updatedAt: string | null;
12
12
  sentAt: string | null;
13
+ lockedAt: string | null;
14
+ lockedBy: string | null;
13
15
  }
14
16
  export declare function useStockPushJobs(limit?: number): import("@tanstack/react-query").UseQueryResult<StockPushJob[], Error>;
15
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useStockPushJobs.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockPushJobs.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAaD,wBAAgB,gBAAgB,CAAC,KAAK,SAAK,yEA4C1C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB;aACT,OAAO;aAAW,MAAM,GAAG,IAAI;UAoB3D"}
1
+ {"version":3,"file":"useStockPushJobs.d.ts","sourceRoot":"","sources":["../../src/hooks/useStockPushJobs.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAwBD,wBAAgB,gBAAgB,CAAC,KAAK,SAAK,yEAgD1C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB;aACT,OAAO;aAAW,MAAM,GAAG,IAAI;UAoB3D"}
@@ -5,13 +5,24 @@
5
5
  */
6
6
  import { useQuery } from "@tanstack/react-query";
7
7
  import { supabase } from "../integrations/supabase/client";
8
+ /** Floriday levert namen soms als taalobject ({ nl: "…", en: "…" }). */
9
+ function plainName(value) {
10
+ if (typeof value === "string")
11
+ return value.trim() || null;
12
+ if (value && typeof value === "object") {
13
+ const first = value.nl ?? value.en ?? Object.values(value)[0];
14
+ if (typeof first === "string")
15
+ return first.trim() || null;
16
+ }
17
+ return null;
18
+ }
8
19
  function nameFromCache(data) {
9
20
  if (!data || typeof data !== "object")
10
21
  return null;
11
- return (data.tradeItemName ??
12
- data.name ??
13
- data.productName ??
14
- data.vbnProductName ??
22
+ return (plainName(data.tradeItemName) ??
23
+ plainName(data.name) ??
24
+ plainName(data.productName) ??
25
+ plainName(data.vbnProductName) ??
15
26
  null);
16
27
  }
17
28
  export function useStockPushJobs(limit = 10) {
@@ -21,7 +32,7 @@ export function useStockPushJobs(limit = 10) {
21
32
  queryFn: async () => {
22
33
  const { data, error } = await supabase
23
34
  .from("floriday_continuous_stock_queue")
24
- .select("id, trade_item_id, number_of_pieces, desired_available, status, attempts, last_error, created_at, updated_at, sent_at")
35
+ .select("id, trade_item_id, number_of_pieces, desired_available, status, attempts, last_error, created_at, updated_at, sent_at, locked_at, locked_by")
25
36
  .order("created_at", { ascending: false })
26
37
  .limit(limit);
27
38
  if (error)
@@ -52,6 +63,8 @@ export function useStockPushJobs(limit = 10) {
52
63
  createdAt: String(r.created_at),
53
64
  updatedAt: r.updated_at ?? null,
54
65
  sentAt: r.sent_at ?? null,
66
+ lockedAt: r.locked_at ?? null,
67
+ lockedBy: r.locked_by ?? null,
55
68
  }));
56
69
  },
57
70
  });