@flowselections/floriday-voorraad 1.0.20 → 1.0.21

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 (64) hide show
  1. package/dist-lib/components/voorraad/ArtikelCompositionCard.d.ts +7 -0
  2. package/dist-lib/components/voorraad/ArtikelCompositionCard.d.ts.map +1 -0
  3. package/dist-lib/components/voorraad/ArtikelCompositionCard.js +196 -0
  4. package/dist-lib/components/voorraad/ArtikelWizard.d.ts +2 -1
  5. package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
  6. package/dist-lib/components/voorraad/ArtikelWizard.js +259 -26
  7. package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
  8. package/dist-lib/components/voorraad/EditArtikelPage.js +28 -85
  9. package/dist-lib/components/voorraad/FloridayStatusBadge.d.ts +4 -0
  10. package/dist-lib/components/voorraad/FloridayStatusBadge.d.ts.map +1 -0
  11. package/dist-lib/components/voorraad/FloridayStatusBadge.js +8 -0
  12. package/dist-lib/components/voorraad/StockBulkButtons.js +7 -6
  13. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +5 -0
  14. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
  15. package/dist-lib/components/voorraad/ValidationOverviewTable.js +26 -56
  16. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts +4 -0
  17. package/dist-lib/components/voorraad/VoorraadDashboard.d.ts.map +1 -1
  18. package/dist-lib/components/voorraad/VoorraadDashboard.js +14 -6
  19. package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
  20. package/dist-lib/components/voorraad/VoorraadStockCard.js +11 -0
  21. package/dist-lib/components/voorraad/VoorraadTable.d.ts.map +1 -1
  22. package/dist-lib/components/voorraad/VoorraadTable.js +5 -12
  23. package/dist-lib/hooks/useFloridayContinuousStock.d.ts +13 -0
  24. package/dist-lib/hooks/useFloridayContinuousStock.d.ts.map +1 -0
  25. package/dist-lib/hooks/useFloridayContinuousStock.js +109 -0
  26. package/dist-lib/hooks/useFlorydayCustomers.d.ts +11 -4
  27. package/dist-lib/hooks/useFlorydayCustomers.d.ts.map +1 -1
  28. package/dist-lib/hooks/useFlorydayCustomers.js +85 -38
  29. package/dist-lib/hooks/useLocalTradeItems.d.ts +26 -2
  30. package/dist-lib/hooks/useLocalTradeItems.d.ts.map +1 -1
  31. package/dist-lib/hooks/useLocalTradeItems.js +119 -79
  32. package/dist-lib/hooks/useVoorraadData.d.ts +5 -5
  33. package/dist-lib/hooks/useVoorraadData.d.ts.map +1 -1
  34. package/dist-lib/hooks/useVoorraadData.js +127 -50
  35. package/dist-lib/integrations/supabase/auth-middleware.d.ts +662 -168
  36. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/client.d.ts +662 -168
  38. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  39. package/dist-lib/integrations/supabase/client.server.d.ts +662 -168
  40. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  41. package/dist-lib/integrations/supabase/types.d.ts +678 -163
  42. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  43. package/dist-lib/lib/floricode-required-features.functions.d.ts +662 -168
  44. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/floriday-client.d.ts +29 -12
  46. package/dist-lib/lib/floriday-client.d.ts.map +1 -1
  47. package/dist-lib/lib/floriday-client.js +36 -0
  48. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +662 -168
  49. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/floriday-gateway.functions.d.ts +3220 -750
  51. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  52. package/dist-lib/lib/floriday-gateway.server.d.ts +14 -0
  53. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  54. package/dist-lib/lib/floriday-gateway.server.js +3 -0
  55. package/dist-lib/lib/floriday-payload.d.ts.map +1 -1
  56. package/dist-lib/lib/floriday-payload.js +32 -8
  57. package/dist-lib/lib/floriday-warehouse.functions.d.ts +1932 -450
  58. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  59. package/dist-lib/lib/floriday-writes.functions.d.ts +1333 -345
  60. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  61. package/dist-lib/lib/merge-products.d.ts +8 -0
  62. package/dist-lib/lib/merge-products.d.ts.map +1 -0
  63. package/dist-lib/lib/merge-products.js +17 -0
  64. package/package.json +2 -2
@@ -22,15 +22,16 @@ const COLUMNS = [
22
22
  "Inkomende voorraad",
23
23
  ];
24
24
  /**
25
- * Determine which trade_item_stock key to write to. The stock table is keyed
26
- * by whatever `products.trade_item_key` (or the draft/floriday id) resolved
27
- * to when the row was created — never by the synthetic UI id `erp:<uuid>`.
25
+ * Bepaalt de sleutel in `trade_item_stock`. Sinds de dedup-migratie is dat altijd
26
+ * `product:<uuid>`; oude `draft:` / `floriday:` waarden zijn ge-repoint.
28
27
  */
29
28
  function toKey(item) {
30
29
  if (item.tradeItemKey)
31
30
  return item.tradeItemKey;
31
+ if (item.productId)
32
+ return `product:${item.productId}`;
32
33
  if (item.localId)
33
- return `draft:${item.localId}`;
34
+ return `product:${item.localId}`;
34
35
  return item.id;
35
36
  }
36
37
  function parseNumber(val) {
@@ -333,7 +334,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
333
334
  },
334
335
  });
335
336
  created++;
336
- // Stock koppelen aan de nieuwe draft
337
+ // Stock koppelen aan het nieuwe product (draftId is nu de product-uuid)
337
338
  const anyStock = stockValues.physical_stock !== null ||
338
339
  stockValues.minimum_stock !== null ||
339
340
  stockValues.reserved_stock !== null ||
@@ -342,7 +343,7 @@ export function StockBulkButtons({ items, activeConnectionId, onImported, }) {
342
343
  stockBatch.push({
343
344
  user_id: userId,
344
345
  connection_id: activeConnectionId,
345
- trade_item_key: `draft:${draftId}`,
346
+ trade_item_key: `product:${draftId}`,
346
347
  physical_stock: stockValues.physical_stock ?? 0,
347
348
  minimum_stock: stockValues.minimum_stock ?? 0,
348
349
  reserved_stock: stockValues.reserved_stock ?? 0,
@@ -1,4 +1,9 @@
1
1
  import type { TradeItem } from "../../lib/floriday-client";
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
+ */
2
7
  export declare function ValidationOverviewTable({ items, connectionId, onChanged, }: {
3
8
  items: TradeItem[];
4
9
  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,EAAsB,MAAM,2BAA2B,CAAC;AAwB/E,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,2CA0IA"}
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;AAM3D;;;;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,2CA4GA"}
@@ -2,86 +2,56 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { useNavigate } from "@tanstack/react-router";
4
4
  import { Badge, Button } from "@flowselections/core";
5
- import { Pencil, RefreshCw, Send } from "lucide-react";
5
+ import { Pencil, Send } from "lucide-react";
6
6
  import { validateDraftForPublish } from "../../lib/floriday-import-validator";
7
7
  import { updateDraftMeta } from "../../hooks/useLocalTradeItems";
8
8
  import { floriday } from "../../lib/floriday-client";
9
9
  import { toast } from "@flowselections/core";
10
- function statusBadge(s) {
11
- switch (s) {
12
- case "linked":
13
- case "published":
14
- return _jsx(Badge, { className: "bg-emerald-500/15 text-emerald-700 border-emerald-500/30", children: "Gekoppeld" });
15
- case "ambiguous_match":
16
- return _jsx(Badge, { className: "bg-amber-500/15 text-amber-700 border-amber-500/30", children: "Meerdere matches" });
17
- case "waiting_for_data":
18
- return _jsx(Badge, { className: "bg-amber-500/15 text-amber-700 border-amber-500/30", children: "Wacht op gegevens" });
19
- case "ready_to_publish":
20
- return _jsx(Badge, { className: "bg-blue-500/15 text-blue-700 border-blue-500/30", children: "Klaar voor publicatie" });
21
- case "publish_failed":
22
- return _jsx(Badge, { variant: "destructive", children: "Publicatie mislukt" });
23
- default:
24
- return _jsx(Badge, { variant: "outline", children: "Niet gekoppeld" });
25
- }
26
- }
10
+ /**
11
+ * Overzicht van producten die (nog) niet actief op Floriday staan. Er is geen concept-status
12
+ * meer — het gaat puur om welke Floriday-velden ontbreken en of het product klaar is om te
13
+ * activeren. Producten blijven volwaardig in gebruik binnen ERP, ongeacht Floriday-status.
14
+ */
27
15
  export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
28
16
  const navigate = useNavigate();
29
- const rows = useMemo(() => items.filter((i) => !i.published &&
30
- i.linkStatus !== "linked" &&
31
- i.linkStatus !== "published"), [items]);
32
- const revalidate = async (item) => {
33
- if (!item.localId)
17
+ const rows = useMemo(() => items.filter((i) => !i.floridayActive), [items]);
18
+ const publish = async (item) => {
19
+ const productId = item.productId ?? item.localId;
20
+ if (!productId || !connectionId)
34
21
  return;
35
- const d = item.raw?.data ?? {};
36
- const v = validateDraftForPublish(d);
37
- try {
38
- await updateDraftMeta(item.localId, {
39
- missingRequiredFields: v.missing,
40
- floridayLinkStatus: v.ok ? "ready_to_publish" : "waiting_for_data",
41
- lastFloridaySyncAt: new Date().toISOString(),
42
- });
43
- toast.success("Validatie bijgewerkt");
44
- await onChanged();
45
- }
46
- catch (e) {
47
- toast.error(e?.message ?? "Bijwerken mislukt");
48
- }
49
- };
50
- const republish = async (item) => {
51
- if (!item.localId || !connectionId)
52
- return;
53
- const d = item.raw?.data;
54
- const v = validateDraftForPublish(d);
22
+ const payload = item.raw?.floriday_payload ?? item.raw?.data ?? {};
23
+ const v = validateDraftForPublish(payload);
55
24
  if (!v.ok) {
56
25
  toast.error("Nog niet compleet: " + v.missing.join(", "));
57
26
  return;
58
27
  }
59
28
  try {
60
29
  const { buildAddTradeItemPayload } = await import("../../lib/floriday-payload");
61
- const payload = buildAddTradeItemPayload(d);
62
- const res = await floriday.createTradeItem(connectionId, payload);
63
- await updateDraftMeta(item.localId, {
30
+ const built = buildAddTradeItemPayload(payload);
31
+ const res = await floriday.createTradeItem(connectionId, built);
32
+ await updateDraftMeta(productId, {
64
33
  floridayLinkStatus: "published",
65
34
  floridayId: res?.tradeItemId ?? null,
66
35
  floridayLinkError: null,
67
36
  lastFloridaySyncAt: new Date().toISOString(),
68
37
  });
69
- toast.success("Gepubliceerd naar Floriday");
38
+ toast.success("Product staat nu actief op Floriday");
70
39
  await onChanged();
71
40
  }
72
41
  catch (e) {
73
- await updateDraftMeta(item.localId, {
74
- floridayLinkStatus: "publish_failed",
75
- floridayLinkError: e?.message ?? String(e),
76
- lastFloridaySyncAt: new Date().toISOString(),
77
- });
78
- toast.error(e?.message ?? "Publicatie mislukt");
42
+ toast.error(e?.message ?? "Activeren op Floriday mislukt");
79
43
  await onChanged();
80
44
  }
81
45
  };
82
46
  if (rows.length === 0) {
83
- return (_jsx("div", { className: "p-8 text-center text-sm text-muted-foreground", children: "Alle artikelen zijn gekoppeld aan Floriday." }));
47
+ return (_jsx("div", { className: "p-8 text-center text-sm text-muted-foreground", children: "Alle producten staan actief op Floriday." }));
84
48
  }
85
- return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3", children: "Artikelcode" }), _jsx("th", { className: "p-3", children: "Artikelnaam" }), _jsx("th", { className: "p-3", children: "Status" }), _jsx("th", { className: "p-3", children: "Ontbrekende gegevens" }), _jsx("th", { className: "p-3", children: "Laatste sync" }), _jsx("th", { className: "p-3", children: "Foutmelding" }), _jsx("th", { className: "p-3 text-right", children: "Acties" })] }) }), _jsx("tbody", { children: rows.map((it) => (_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: statusBadge(it.linkStatus) }), _jsx("td", { className: "p-3", children: (it.missingFields ?? []).length === 0 ? (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" })) : (_jsx("div", { className: "flex flex-wrap gap-1", children: (it.missingFields ?? []).map((m) => (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: m }, m))) })) }), _jsx("td", { className: "p-3 text-xs text-muted-foreground", children: it.lastSyncAt ? new Date(it.lastSyncAt).toLocaleString("nl-NL") : "—" }), _jsx("td", { className: "p-3 text-xs text-destructive max-w-[240px] truncate", children: it.linkError ?? "—" }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => it.localId &&
86
- navigate({ to: "/voorraad/$id", params: { id: it.localId } }), children: [_jsx(Pencil, { className: "h-3 w-3 mr-1" }), "Bewerken"] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => revalidate(it), children: [_jsx(RefreshCw, { className: "h-3 w-3 mr-1" }), "Valideer"] }), _jsxs(Button, { size: "sm", onClick: () => republish(it), children: [_jsx(Send, { className: "h-3 w-3 mr-1" }), "Publiceer"] })] }) })] }, it.id))) })] }) }));
49
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3", children: "Artikelcode" }), _jsx("th", { className: "p-3", children: "Artikelnaam" }), _jsx("th", { className: "p-3", children: "Floriday-status" }), _jsx("th", { className: "p-3", children: "Ontbrekende Floriday-velden" }), _jsx("th", { className: "p-3 text-right", children: "Acties" })] }) }), _jsx("tbody", { children: rows.map((it) => {
50
+ const payload = it.raw?.floriday_payload ?? it.raw?.data ?? {};
51
+ const v = validateDraftForPublish(payload);
52
+ 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", children: v.missing.length === 0 ? (_jsx("span", { className: "text-xs text-emerald-700", children: "Klaar om te activeren" })) : (_jsx("div", { className: "flex flex-wrap gap-1", children: v.missing.map((m) => (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: m }, m))) })) }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
53
+ const target = it.productId ?? it.localId ?? it.id;
54
+ navigate({ to: "/voorraad/$id", params: { id: target } });
55
+ }, children: [_jsx(Pencil, { className: "h-3 w-3 mr-1" }), "Bewerken"] }), _jsxs(Button, { size: "sm", onClick: () => publish(it), disabled: !v.ok, children: [_jsx(Send, { className: "h-3 w-3 mr-1" }), "Activeer op Floriday"] })] }) })] }, it.id));
56
+ }) })] }) }));
87
57
  }
@@ -1,4 +1,8 @@
1
1
  import type { TradeItem } from "../../lib/floriday-client";
2
+ /**
3
+ * Producten zijn altijd volwaardig. De dashboard-statistieken tonen alleen of ze op
4
+ * Floriday actief zijn — geen concept-status meer.
5
+ */
2
6
  export declare function VoorraadDashboard({ items, connectionId, }: {
3
7
  items: TradeItem[];
4
8
  connectionId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadDashboard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadDashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AA2B3D,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,YAAY,GACb,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,2CA4DA"}
1
+ {"version":3,"file":"VoorraadDashboard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadDashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AA4B3D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,YAAY,GACb,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,2CAuDA"}
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Link } from "@tanstack/react-router";
3
3
  import { Card } from "@flowselections/core";
4
+ import { validateDraftForPublish } from "../../lib/floriday-import-validator";
4
5
  function Stat({ label, value, tone = "default", }) {
5
6
  const toneCls = tone === "danger"
6
7
  ? "text-destructive"
@@ -11,13 +12,20 @@ function Stat({ label, value, tone = "default", }) {
11
12
  : "text-foreground";
12
13
  return (_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: `text-2xl font-semibold tabular-nums ${toneCls}`, children: value })] }));
13
14
  }
15
+ /**
16
+ * Producten zijn altijd volwaardig. De dashboard-statistieken tonen alleen of ze op
17
+ * Floriday actief zijn — geen concept-status meer.
18
+ */
14
19
  export function VoorraadDashboard({ items, connectionId, }) {
15
20
  const total = items.length;
16
- const linked = items.filter((i) => i.published).length +
17
- items.filter((i) => !i.published && i.linkStatus === "linked").length;
18
- const waiting = items.filter((i) => !i.published && i.linkStatus === "waiting_for_data").length;
19
- const validationErrors = items.filter((i) => !i.published && (i.missingFields?.length ?? 0) > 0 && i.linkStatus !== "linked").length;
20
- const publishErrors = items.filter((i) => !i.published && i.linkStatus === "publish_failed").length;
21
+ const active = items.filter((i) => i.floridayActive).length;
22
+ const inactive = total - active;
23
+ const missingFields = items.filter((i) => {
24
+ if (i.floridayActive)
25
+ return false;
26
+ const payload = i.raw?.floriday_payload ?? i.raw?.data ?? {};
27
+ return !validateDraftForPublish(payload).ok;
28
+ }).length;
21
29
  // Nieuw sinds laatste import
22
30
  let lastImport = null;
23
31
  try {
@@ -38,5 +46,5 @@ export function VoorraadDashboard({ items, connectionId, }) {
38
46
  return src === "excel_import" && created && created >= lastImport;
39
47
  }).length
40
48
  : 0;
41
- return (_jsxs(Card, { className: "p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3 mb-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Floriday-koppeling overzicht" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Status van alle artikelen ten opzichte van Floriday" })] }), _jsx(Link, { to: "/voorraad/validatie", className: "text-xs text-primary hover:underline", children: "Openstaande artikelen bekijken \u2192" })] }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4", children: [_jsx(Stat, { label: "Artikelen", value: total }), _jsx(Stat, { label: "Gekoppeld", value: linked, tone: "good" }), _jsx(Stat, { label: "Wacht op koppeling", value: waiting, tone: "warn" }), _jsx(Stat, { label: "Validatiefouten", value: validationErrors, tone: "warn" }), _jsx(Stat, { label: "Publicatiefouten", value: publishErrors, tone: "danger" }), _jsx(Stat, { label: "Nieuw sinds import", value: newSinceImport })] })] }));
49
+ return (_jsxs(Card, { className: "p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3 mb-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Voorraad-overzicht" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Alle producten zijn volwaardig; alleen de Floriday-activatie verschilt." })] }), _jsx(Link, { to: "/voorraad/validatie", className: "text-xs text-primary hover:underline", children: "Producten zonder Floriday-activatie \u2192" })] }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4", children: [_jsx(Stat, { label: "Producten", value: total }), _jsx(Stat, { label: "Actief op Floriday", value: active, tone: "good" }), _jsx(Stat, { label: "Niet actief op Floriday", value: inactive }), _jsx(Stat, { label: "Ontbrekende Floriday-velden", value: missingFields, tone: "warn" }), _jsx(Stat, { label: "Nieuw sinds import", value: newSinceImport })] })] }));
42
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AAaA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,KAAK,2CAqGzF"}
1
+ {"version":3,"file":"VoorraadStockCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadStockCard.tsx"],"names":[],"mappings":"AAcA,UAAU,KAAK;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,KAAK,2CAsHzF"}
@@ -3,15 +3,26 @@ import { useEffect, useState } from "react";
3
3
  import { Button, Card, CardContent, Input, Label, Textarea, toast, } from "@flowselections/core";
4
4
  import { Loader2, Package, Check } from "lucide-react";
5
5
  import { useTradeItemStock, stockKeysFor } from "../../hooks/useTradeItemStock";
6
+ import { useFloridayContinuousStock } from "../../hooks/useFloridayContinuousStock";
6
7
  export function VoorraadStockCard({ connectionId, tradeItemId, draftId, productId }) {
7
8
  const keys = stockKeysFor({ tradeItemId, draftId, productId });
8
9
  const key = keys[0] ?? null;
9
10
  const { row, loading, save } = useTradeItemStock(connectionId, keys);
10
11
  const [values, setValues] = useState(row);
11
12
  const [saving, setSaving] = useState(false);
13
+ const floridayPieces = useFloridayContinuousStock(connectionId, tradeItemId ?? null);
12
14
  useEffect(() => {
13
15
  setValues(row);
14
16
  }, [row.updated_at, row.physical_stock, row.minimum_stock, row.reserved_stock, row.incoming_stock, row.notes]);
17
+ // Silent sync: als Floriday een nieuwe voorraad terugmeldt, overschrijven
18
+ // we het "Liggend"-veld zonder extra UI-signaal.
19
+ useEffect(() => {
20
+ if (floridayPieces === undefined)
21
+ return;
22
+ setValues((v) => v.physical_stock === floridayPieces
23
+ ? v
24
+ : { ...v, physical_stock: floridayPieces });
25
+ }, [floridayPieces]);
15
26
  if (!key) {
16
27
  return (_jsx(Card, { className: "p-4 text-sm text-muted-foreground", children: "Voorraad kan pas worden ingesteld nadat het artikel voor het eerst is opgeslagen." }));
17
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VoorraadTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadTable.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAW3D,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,2CAmHA"}
1
+ {"version":3,"file":"VoorraadTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadTable.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAY3D,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,2CAmHA"}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useNavigate } from "@tanstack/react-router";
3
- import { Badge } from "@flowselections/core";
4
3
  import { Package } from "lucide-react";
4
+ import { Badge } from "@flowselections/core";
5
5
  const columns = [
6
6
  { key: "quantity", label: "Liggend" },
7
7
  { key: "minQuantity", label: "Minimum" },
@@ -18,17 +18,10 @@ export function VoorraadTable({ items, loading, }) {
18
18
  if (items.length === 0) {
19
19
  return (_jsx("p", { className: "text-sm text-muted-foreground p-6", children: "Geen producten gevonden voor deze connectie." }));
20
20
  }
21
- return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3 w-14" }), _jsx("th", { className: "p-3", children: "Product" }), _jsx("th", { className: "p-3", children: "Artikelnummer" }), columns.map((c) => (_jsx("th", { className: "p-3 text-right whitespace-nowrap", children: c.label }, c.key)))] }) }), _jsx("tbody", { children: items.map((item) => {
22
- const isLocal = !item.published;
23
- const isErp = item.source === "erp";
24
- const low = !isLocal &&
25
- item.minQuantity > 0 &&
21
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left", children: _jsxs("tr", { children: [_jsx("th", { className: "p-3 w-14" }), _jsx("th", { className: "p-3", children: "Product" }), _jsx("th", { className: "p-3", children: "Type" }), _jsx("th", { className: "p-3", children: "Artikelnummer" }), columns.map((c) => (_jsx("th", { className: "p-3 text-right whitespace-nowrap", children: c.label }, c.key)))] }) }), _jsx("tbody", { children: items.map((item) => {
22
+ const low = item.minQuantity > 0 &&
26
23
  item.availableQuantity < item.minQuantity;
27
- const linkId = isErp
28
- ? item.productId
29
- : isLocal
30
- ? item.localId
31
- : item.id;
24
+ const linkId = item.productId ?? item.localId ?? item.id;
32
25
  const clickable = Boolean(linkId);
33
26
  const open = () => {
34
27
  if (linkId)
@@ -41,6 +34,6 @@ export function VoorraadTable({ items, loading, }) {
41
34
  open();
42
35
  }
43
36
  }
44
- : undefined, "aria-label": clickable ? `Bewerk ${item.name}` : undefined, children: [_jsx("td", { className: "p-2", children: item.imageUrl ? (_jsx("img", { src: item.imageUrl, alt: item.name, className: "h-10 w-10 rounded object-cover", loading: "lazy" })) : (_jsx("div", { className: "h-10 w-10 rounded bg-muted flex items-center justify-center", children: _jsx(Package, { className: "h-4 w-4 text-muted-foreground" }) })) }), _jsxs("td", { className: "p-3", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "font-medium text-foreground", children: item.name }), isLocal && (_jsx(Badge, { variant: "outline", className: "text-[10px] font-normal text-muted-foreground border-amber-500/40 bg-amber-500/10", children: "Niet gepubliceerd" }))] }), low && (_jsx("div", { className: "text-xs text-destructive", children: "onder minimumvoorraad" }))] }), _jsx("td", { className: "p-3 font-mono text-xs", children: item.articleNumber || "—" }), columns.map((c) => (_jsx("td", { className: "p-3 text-right tabular-nums text-muted-foreground", children: Number(item[c.key] ?? 0) }, c.key)))] }, item.id));
37
+ : undefined, "aria-label": clickable ? `Bewerk ${item.name}` : undefined, children: [_jsx("td", { className: "p-2", children: item.imageUrl ? (_jsx("img", { src: item.imageUrl, alt: item.name, className: "h-10 w-10 rounded object-cover", loading: "lazy" })) : (_jsx("div", { className: "h-10 w-10 rounded bg-muted flex items-center justify-center", children: _jsx(Package, { className: "h-4 w-4 text-muted-foreground" }) })) }), _jsxs("td", { className: "p-3", children: [_jsx("div", { className: "flex items-center gap-2 flex-wrap", children: _jsx("span", { className: "font-medium text-foreground", children: item.name }) }), low && (_jsx("div", { className: "text-xs text-destructive", children: "onder minimumvoorraad" }))] }), _jsx("td", { className: "p-3", children: item.itemType === "productgroep" ? (_jsx(Badge, { className: "bg-indigo-500/15 text-indigo-700 border-indigo-500/30", children: "Productgroep" })) : item.itemType === "samenstelling" ? (_jsx(Badge, { className: "bg-emerald-500/15 text-emerald-700 border-emerald-500/30", children: "Samenstelling" })) : (_jsx(Badge, { variant: "secondary", children: "Artikel" })) }), _jsx("td", { className: "p-3 font-mono text-xs", children: item.articleNumber || "—" }), columns.map((c) => (_jsx("td", { className: "p-3 text-right tabular-nums text-muted-foreground", children: Number(item[c.key] ?? 0) }, c.key)))] }, item.id));
45
38
  }) })] }) }));
46
39
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Silent live-cache van Floriday `continuous-stock.numberOfPieces` per
3
+ * trade_item_id, geïndexeerd binnen één connectie. Vult zichzelf via
4
+ * Supabase realtime — er zijn geen extra UI-elementen nodig.
5
+ */
6
+ export declare function useFloridayContinuousStockMap(connectionId: string | null): Map<string, number>;
7
+ /**
8
+ * Enkelvoudige variant voor per-artikel view (VoorraadStockCard). Retourneert
9
+ * `undefined` zolang er geen cache-waarde bekend is; realtime updates komen
10
+ * vanzelf binnen zonder extra UI.
11
+ */
12
+ export declare function useFloridayContinuousStock(connectionId: string | null, tradeItemId: string | null | undefined): number | undefined;
13
+ //# sourceMappingURL=useFloridayContinuousStock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFloridayContinuousStock.d.ts","sourceRoot":"","sources":["../../src/hooks/useFloridayContinuousStock.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA8DrB;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,GAAG,SAAS,CAqDpB"}
@@ -0,0 +1,109 @@
1
+ import { useEffect, useState } from "react";
2
+ import { supabase } from "../integrations/supabase/client";
3
+ /**
4
+ * Silent live-cache van Floriday `continuous-stock.numberOfPieces` per
5
+ * trade_item_id, geïndexeerd binnen één connectie. Vult zichzelf via
6
+ * Supabase realtime — er zijn geen extra UI-elementen nodig.
7
+ */
8
+ export function useFloridayContinuousStockMap(connectionId) {
9
+ const [map, setMap] = useState(new Map());
10
+ useEffect(() => {
11
+ if (!connectionId) {
12
+ setMap(new Map());
13
+ return;
14
+ }
15
+ let cancelled = false;
16
+ const load = async () => {
17
+ const { data, error } = await supabase
18
+ .from("floriday_continuous_stock_cache")
19
+ .select("trade_item_id, number_of_pieces")
20
+ .eq("connection_id", connectionId);
21
+ if (cancelled || error || !data)
22
+ return;
23
+ const next = new Map();
24
+ for (const r of data) {
25
+ next.set(r.trade_item_id, r.number_of_pieces);
26
+ }
27
+ setMap(next);
28
+ };
29
+ load();
30
+ const channel = supabase
31
+ .channel(`floriday-continuous-stock-${connectionId}`)
32
+ .on("postgres_changes", {
33
+ event: "*",
34
+ schema: "public",
35
+ table: "floriday_continuous_stock_cache",
36
+ filter: `connection_id=eq.${connectionId}`,
37
+ }, (payload) => {
38
+ setMap((prev) => {
39
+ const next = new Map(prev);
40
+ const row = payload.new ?? payload.old ?? {};
41
+ const id = row.trade_item_id;
42
+ if (!id)
43
+ return prev;
44
+ if (payload.eventType === "DELETE") {
45
+ next.delete(id);
46
+ }
47
+ else if (typeof row.number_of_pieces === "number") {
48
+ next.set(id, row.number_of_pieces);
49
+ }
50
+ return next;
51
+ });
52
+ })
53
+ .subscribe();
54
+ return () => {
55
+ cancelled = true;
56
+ supabase.removeChannel(channel);
57
+ };
58
+ }, [connectionId]);
59
+ return map;
60
+ }
61
+ /**
62
+ * Enkelvoudige variant voor per-artikel view (VoorraadStockCard). Retourneert
63
+ * `undefined` zolang er geen cache-waarde bekend is; realtime updates komen
64
+ * vanzelf binnen zonder extra UI.
65
+ */
66
+ export function useFloridayContinuousStock(connectionId, tradeItemId) {
67
+ const [value, setValue] = useState(undefined);
68
+ useEffect(() => {
69
+ if (!connectionId || !tradeItemId) {
70
+ setValue(undefined);
71
+ return;
72
+ }
73
+ let cancelled = false;
74
+ const load = async () => {
75
+ const { data } = await supabase
76
+ .from("floriday_continuous_stock_cache")
77
+ .select("number_of_pieces")
78
+ .eq("connection_id", connectionId)
79
+ .eq("trade_item_id", tradeItemId)
80
+ .maybeSingle();
81
+ if (cancelled)
82
+ return;
83
+ if (data && typeof data.number_of_pieces === "number") {
84
+ setValue(data.number_of_pieces);
85
+ }
86
+ };
87
+ load();
88
+ const channel = supabase
89
+ .channel(`floriday-cs-${connectionId}-${tradeItemId}`)
90
+ .on("postgres_changes", {
91
+ event: "*",
92
+ schema: "public",
93
+ table: "floriday_continuous_stock_cache",
94
+ filter: `trade_item_id=eq.${tradeItemId}`,
95
+ }, (payload) => {
96
+ const row = payload.new ?? {};
97
+ if (row.connection_id === connectionId &&
98
+ typeof row.number_of_pieces === "number") {
99
+ setValue(row.number_of_pieces);
100
+ }
101
+ })
102
+ .subscribe();
103
+ return () => {
104
+ cancelled = true;
105
+ supabase.removeChannel(channel);
106
+ };
107
+ }, [connectionId, tradeItemId]);
108
+ return value;
109
+ }
@@ -2,12 +2,19 @@ export interface FloridayCustomer {
2
2
  id: string;
3
3
  name: string;
4
4
  hasRealName: boolean;
5
+ city: string | null;
6
+ country: string | null;
7
+ commercialName: string | null;
8
+ logoUrl: string | null;
9
+ gln: string | null;
5
10
  }
6
11
  /**
7
- * Reads buyer organisations from the shared `floriday_connections_cache`
8
- * (which contains buyer org id + name per supplier connection).
9
- * If `buyer_organization_name` is NULL (Auth-module hasn't synced names yet),
10
- * we fall back to a short, readable label so the UI never shows a raw UUID.
12
+ * Buyer organisations available on the current supplier connection.
13
+ *
14
+ * Primary source: `floriday_connections_cache` (which buyers this supplier
15
+ * is actually connected to). We enrich every buyer with data from
16
+ * `floriday_customers_cache` (28k+ orgs) — that's where the real company
17
+ * name, city, country, and logo live.
11
18
  */
12
19
  export declare function useFlorydayCustomers(connectionId: string | null): {
13
20
  customers: FloridayCustomer[];
@@ -1 +1 @@
1
- {"version":3,"file":"useFlorydayCustomers.d.ts","sourceRoot":"","sources":["../../src/hooks/useFlorydayCustomers.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;CACtB;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;EAoD/D"}
1
+ {"version":3,"file":"useFlorydayCustomers.d.ts","sourceRoot":"","sources":["../../src/hooks/useFlorydayCustomers.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAMD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;;;;EA6G/D"}
@@ -4,10 +4,12 @@ function shortId(id) {
4
4
  return `Klant ${id.slice(0, 8)}…${id.slice(-4)}`;
5
5
  }
6
6
  /**
7
- * Reads buyer organisations from the shared `floriday_connections_cache`
8
- * (which contains buyer org id + name per supplier connection).
9
- * If `buyer_organization_name` is NULL (Auth-module hasn't synced names yet),
10
- * we fall back to a short, readable label so the UI never shows a raw UUID.
7
+ * Buyer organisations available on the current supplier connection.
8
+ *
9
+ * Primary source: `floriday_connections_cache` (which buyers this supplier
10
+ * is actually connected to). We enrich every buyer with data from
11
+ * `floriday_customers_cache` (28k+ orgs) — that's where the real company
12
+ * name, city, country, and logo live.
11
13
  */
12
14
  export function useFlorydayCustomers(connectionId) {
13
15
  const [customers, setCustomers] = useState([]);
@@ -21,42 +23,87 @@ export function useFlorydayCustomers(connectionId) {
21
23
  }
22
24
  setLoading(true);
23
25
  setError(null);
24
- supabase
25
- .from("floriday_connections_cache")
26
- .select("buyer_organization_id,buyer_organization_name")
27
- .eq("connection_id", connectionId)
28
- .eq("is_deleted", false)
29
- .then(({ data, error }) => {
30
- if (!mounted)
31
- return;
32
- if (error) {
33
- setError(error.message);
34
- setCustomers([]);
35
- return;
36
- }
37
- const seen = new Set();
38
- const out = [];
39
- for (const row of data ?? []) {
40
- const id = row.buyer_organization_id;
41
- if (!id || seen.has(id))
42
- continue;
43
- seen.add(id);
44
- const real = (row.buyer_organization_name ?? "").trim();
45
- out.push({
46
- id,
47
- name: real.length > 0 ? real : shortId(id),
48
- hasRealName: real.length > 0,
26
+ (async () => {
27
+ try {
28
+ // 1) Buyer IDs op deze connectie
29
+ const { data: conns, error: connErr } = await supabase
30
+ .from("floriday_connections_cache")
31
+ .select("buyer_organization_id,buyer_organization_name")
32
+ .eq("connection_id", connectionId)
33
+ .eq("is_deleted", false);
34
+ if (connErr)
35
+ throw new Error(connErr.message);
36
+ const seen = new Set();
37
+ const buyerRows = [];
38
+ for (const row of conns ?? []) {
39
+ const id = row.buyer_organization_id;
40
+ if (!id || seen.has(id))
41
+ continue;
42
+ seen.add(id);
43
+ buyerRows.push({
44
+ id,
45
+ fallbackName: (row.buyer_organization_name ?? "").trim() || null,
46
+ });
47
+ }
48
+ // 2) Verrijk met naam/adres/logo uit de globale klantencache.
49
+ const enrichment = new Map();
50
+ const ids = buyerRows.map((r) => r.id);
51
+ const CHUNK = 500;
52
+ for (let i = 0; i < ids.length; i += CHUNK) {
53
+ const slice = ids.slice(i, i + CHUNK);
54
+ const { data: rows, error: cErr } = await supabase
55
+ .from("floriday_customers_cache")
56
+ .select("organization_id,name,gln_code,data")
57
+ .in("organization_id", slice);
58
+ if (cErr)
59
+ throw new Error(cErr.message);
60
+ for (const r of rows ?? []) {
61
+ const d = (r.data ?? {});
62
+ const addr = d.mailingAddress ?? d.physicalAddress ?? {};
63
+ enrichment.set(r.organization_id, {
64
+ name: (r.name ?? "").trim() || (d.name ?? "").trim() || null,
65
+ city: addr.city ? String(addr.city).trim() : null,
66
+ country: addr.countryCode ? String(addr.countryCode).trim() : null,
67
+ commercialName: d.commercialName ? String(d.commercialName).trim() : null,
68
+ logoUrl: d.logoUrl ? String(d.logoUrl) : null,
69
+ gln: r.gln_code ?? d.companyGln ?? null,
70
+ });
71
+ }
72
+ }
73
+ const out = buyerRows.map(({ id, fallbackName }) => {
74
+ const e = enrichment.get(id);
75
+ const real = (e?.name ?? fallbackName ?? "").trim();
76
+ return {
77
+ id,
78
+ name: real.length > 0 ? real : shortId(id),
79
+ hasRealName: real.length > 0,
80
+ city: e?.city ?? null,
81
+ country: e?.country ?? null,
82
+ commercialName: e?.commercialName ?? null,
83
+ logoUrl: e?.logoUrl ?? null,
84
+ gln: e?.gln ?? null,
85
+ };
86
+ });
87
+ // Echte namen eerst (alfabetisch), fallback-labels erachter.
88
+ out.sort((a, b) => {
89
+ if (a.hasRealName !== b.hasRealName)
90
+ return a.hasRealName ? -1 : 1;
91
+ return a.name.localeCompare(b.name);
49
92
  });
93
+ if (mounted)
94
+ setCustomers(out);
95
+ }
96
+ catch (e) {
97
+ if (mounted) {
98
+ setError(e.message);
99
+ setCustomers([]);
100
+ }
101
+ }
102
+ finally {
103
+ if (mounted)
104
+ setLoading(false);
50
105
  }
51
- // Real names first (alphabetical), then fallback labels.
52
- out.sort((a, b) => {
53
- if (a.hasRealName !== b.hasRealName)
54
- return a.hasRealName ? -1 : 1;
55
- return a.name.localeCompare(b.name);
56
- });
57
- setCustomers(out);
58
- })
59
- .then(() => mounted && setLoading(false));
106
+ })();
60
107
  return () => {
61
108
  mounted = false;
62
109
  };