@flowselections/floriday-klanten-module 1.0.34 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +11 -0
  2. package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts +5 -0
  3. package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts.map +1 -0
  4. package/dist-lib/components/klanten/CustomerAssortmentTab.js +74 -0
  5. package/dist-lib/components/klanten/FloridayPriceImportDialog.d.ts +5 -0
  6. package/dist-lib/components/klanten/FloridayPriceImportDialog.d.ts.map +1 -0
  7. package/dist-lib/components/klanten/FloridayPriceImportDialog.js +55 -0
  8. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
  9. package/dist-lib/components/klanten/GroupedKlantDetail.js +4 -3
  10. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
  11. package/dist-lib/components/klanten/KlantDetail.js +10 -2
  12. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  13. package/dist-lib/components/klanten/KlantenOverzicht.js +24 -3
  14. package/dist-lib/components/klanten/pricegroups/CustomerPriceGroupSelect.d.ts +4 -0
  15. package/dist-lib/components/klanten/pricegroups/CustomerPriceGroupSelect.d.ts.map +1 -0
  16. package/dist-lib/components/klanten/pricegroups/CustomerPriceGroupSelect.js +47 -0
  17. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts.map +1 -1
  18. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.js +2 -1
  19. package/dist-lib/components/klanten/pricegroups/PriceGroupItemPrices.d.ts +5 -0
  20. package/dist-lib/components/klanten/pricegroups/PriceGroupItemPrices.d.ts.map +1 -0
  21. package/dist-lib/components/klanten/pricegroups/PriceGroupItemPrices.js +83 -0
  22. package/dist-lib/index.d.ts +8 -0
  23. package/dist-lib/index.d.ts.map +1 -1
  24. package/dist-lib/index.js +8 -0
  25. package/dist-lib/integrations/supabase/auth-middleware.d.ts +595 -0
  26. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  27. package/dist-lib/integrations/supabase/client.d.ts +595 -0
  28. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/client.server.d.ts +595 -0
  30. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  31. package/dist-lib/integrations/supabase/types.d.ts +597 -0
  32. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  33. package/dist-lib/lib/accounting/twinfield/mapping.js +13 -13
  34. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1785 -0
  35. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/accounting.functions.d.ts +1785 -0
  37. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  38. package/dist-lib/lib/customer-analytics.functions.d.ts +595 -0
  39. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/customer-assortment.functions.d.ts +26666 -0
  41. package/dist-lib/lib/customer-assortment.functions.d.ts.map +1 -0
  42. package/dist-lib/lib/customer-assortment.functions.js +280 -0
  43. package/dist-lib/lib/customer-notes.functions.d.ts +1190 -0
  44. package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/customer-revenue.functions.d.ts +595 -0
  46. package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/customers.functions.d.ts +5355 -0
  48. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  49. package/dist-lib/lib/delivery-locations.functions.d.ts +2975 -0
  50. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  51. package/dist-lib/lib/errors.d.ts +12 -0
  52. package/dist-lib/lib/errors.d.ts.map +1 -1
  53. package/dist-lib/lib/errors.js +24 -0
  54. package/dist-lib/lib/external-customers.functions.d.ts +2380 -0
  55. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  56. package/dist-lib/lib/floriday-assortment.functions.d.ts +595 -0
  57. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
  58. package/dist-lib/lib/floriday-catalog-prices.d.ts +33 -0
  59. package/dist-lib/lib/floriday-catalog-prices.d.ts.map +1 -0
  60. package/dist-lib/lib/floriday-catalog-prices.js +98 -0
  61. package/dist-lib/lib/floriday-discounts.functions.d.ts +1190 -0
  62. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  63. package/dist-lib/lib/floriday-price-import.functions.d.ts +53265 -0
  64. package/dist-lib/lib/floriday-price-import.functions.d.ts.map +1 -0
  65. package/dist-lib/lib/floriday-price-import.functions.js +219 -0
  66. package/dist-lib/lib/floriday-trade-items.functions.d.ts +595 -0
  67. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
  68. package/dist-lib/lib/floriday.server.d.ts +55 -0
  69. package/dist-lib/lib/floriday.server.d.ts.map +1 -0
  70. package/dist-lib/lib/floriday.server.js +765 -0
  71. package/dist-lib/lib/local-discounts.functions.d.ts +4165 -0
  72. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  73. package/dist-lib/lib/logistics-events.functions.d.ts +595 -0
  74. package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
  75. package/dist-lib/lib/logistics-strategy.functions.d.ts +2380 -0
  76. package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
  77. package/dist-lib/lib/overdue-invoices.functions.d.ts +595 -0
  78. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  79. package/dist-lib/lib/price-groups-local.functions.d.ts +137905 -27
  80. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
  81. package/dist-lib/lib/price-groups-local.functions.js +145 -0
  82. package/dist-lib/lib/price-groups.functions.d.ts +601 -1
  83. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
  84. package/dist-lib/lib/twinfield-adopt.functions.d.ts +1190 -0
  85. package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
  86. package/dist-lib/lib/twinfield-ledger.functions.d.ts +595 -0
  87. package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
  88. package/dist-lib/lib/twinfield-ledger.server.js +21 -21
  89. package/package.json +1 -1
  90. package/public/flowselections-assets/Klanten/twinfield-logo.svg +217 -217
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 1,
3
+ "asset_id": "3e27c03f-e308-40e7-ac35-07bf1161c1e2",
4
+ "project_id": "dfdc14cf-30fe-4e0b-8abc-a3ae973943d8",
5
+ "url": "/__l5e/assets-v1/3e27c03f-e308-40e7-ac35-07bf1161c1e2/floriday-logo.png",
6
+ "r2_key": "a/v1/dfdc14cf-30fe-4e0b-8abc-a3ae973943d8/3e27c03f-e308-40e7-ac35-07bf1161c1e2/floriday-logo.png",
7
+ "original_filename": "floriday-logo.png",
8
+ "size": 6921,
9
+ "content_type": "image/png",
10
+ "created_at": "2026-07-07T13:17:11Z"
11
+ }
@@ -0,0 +1,5 @@
1
+ export declare function CustomerAssortmentTab({ customerKey, organizationId, }: {
2
+ customerKey: string;
3
+ organizationId?: string | null;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=CustomerAssortmentTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerAssortmentTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerAssortmentTab.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,cAAc,GACf,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,+BA8LA"}
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Card, CardContent, Badge, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
6
+ import { Search, ImageOff } from "lucide-react";
7
+ import { listCustomerAssortment } from "../../lib/customer-assortment.functions";
8
+ import { decodeDeep } from "../../lib/decode-html";
9
+ import { friendlyError } from "../../lib/errors";
10
+ import { FloridayPriceImportDialog } from "./FloridayPriceImportDialog";
11
+ function money(value, currency) {
12
+ if (value == null)
13
+ return "—";
14
+ try {
15
+ return new Intl.NumberFormat("nl-NL", { style: "currency", currency }).format(value);
16
+ }
17
+ catch {
18
+ return `${currency} ${value.toFixed(2)}`;
19
+ }
20
+ }
21
+ export function CustomerAssortmentTab({ customerKey, organizationId, }) {
22
+ const list = useServerFn(listCustomerAssortment);
23
+ const [search, setSearch] = useState("");
24
+ const [onlyPriced, setOnlyPriced] = useState(false);
25
+ const { data, isLoading, error } = useQuery({
26
+ queryKey: ["customer-assortment", customerKey, organizationId ?? null],
27
+ queryFn: () => list({ data: { customerKey, organizationId: organizationId ?? null } }).then(decodeDeep),
28
+ enabled: !!customerKey,
29
+ staleTime: 60000,
30
+ });
31
+ const items = data?.items ?? [];
32
+ const filtered = useMemo(() => {
33
+ const q = search.trim().toLowerCase();
34
+ return items.filter((i) => {
35
+ if (onlyPriced && i.price == null)
36
+ return false;
37
+ if (!q)
38
+ return true;
39
+ return (i.name.toLowerCase().includes(q) ||
40
+ (i.supplier_article_code ?? "").toLowerCase().includes(q) ||
41
+ (i.vbn_code ?? "").toLowerCase().includes(q) ||
42
+ (i.botanical_name ?? "").toLowerCase().includes(q));
43
+ });
44
+ }, [items, search, onlyPriced]);
45
+ if (isLoading) {
46
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "py-8 text-sm text-muted-foreground", children: "Assortiment laden\u2026" }) }));
47
+ }
48
+ if (error) {
49
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "py-6 text-sm text-destructive", children: friendlyError(error) }) }));
50
+ }
51
+ const pg = data?.priceGroup ?? null;
52
+ const currency = pg?.currency ?? "EUR";
53
+ if (items.length === 0) {
54
+ return (_jsx(Card, { children: _jsxs(CardContent, { className: "py-10 text-center space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Nog geen assortiment beschikbaar" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Er staan nog geen handelsartikelen uit Floriday klaar voor deze klant." })] }) }));
55
+ }
56
+ return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: data?.summary.total ?? 0 }), " artikelen"] }), _jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: data?.summary.priced ?? 0 }), " met prijs"] }), pg ? (_jsxs("span", { children: ["Prijsgroep: ", _jsx("strong", { className: "text-foreground", children: pg.name }), pg.pricing_mode === "calculated" && pg.calc_value
57
+ ? ` (${pg.calc_type === "surcharge" ? "toeslag" : "korting"} ${pg.calc_value}${pg.calc_value_type === "percentage" ? "%" : ` ${currency}`})`
58
+ : ""] })) : (_jsx("span", { className: "text-amber-600", children: "Geen prijsgroep ingesteld voor deze klant" })), data?.summary.last_synced_at && (_jsxs("span", { children: ["laatste sync: ", new Date(data.summary.last_synced_at).toLocaleString("nl-NL")] }))] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op naam, artikelcode of VBN-code\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsx("button", { type: "button", onClick: () => setOnlyPriced((v) => !v), className: "inline-flex items-center rounded-full border px-3 py-1 text-xs transition-colors " +
59
+ (onlyPriced
60
+ ? "bg-primary text-primary-foreground border-primary"
61
+ : "bg-card text-muted-foreground border-border hover:bg-muted"), children: "Alleen met prijs" }), _jsx("div", { className: "ml-auto", children: _jsx(FloridayPriceImportDialog, { customerKey: customerKey, organizationId: organizationId }) })] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[64px]" }), _jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { children: "Verpakking" }), _jsx(TableHead, { className: "text-right", children: "Basisprijs" }), _jsx(TableHead, { className: "text-right", children: "Prijs voor klant" })] }) }), _jsx(TableBody, { children: filtered.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center py-6 text-muted-foreground text-sm", children: "Geen resultaten voor deze zoekopdracht." }) })) : filtered.map((i) => {
62
+ const packing = [
63
+ i.pieces_per_package != null ? `${i.pieces_per_package} per fust` : null,
64
+ i.packages_per_layer != null ? `${i.packages_per_layer} per laag` : null,
65
+ i.layers_per_carrier != null ? `${i.layers_per_carrier} lagen` : null,
66
+ ].filter(Boolean);
67
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: i.image_url ? (_jsx("img", { src: i.image_url, alt: "", loading: "lazy", className: "h-10 w-10 rounded object-cover bg-muted" })) : (_jsx("div", { className: "h-10 w-10 rounded bg-muted flex items-center justify-center text-muted-foreground", children: _jsx(ImageOff, { className: "h-4 w-4" }) })) }), _jsxs(TableCell, { children: [_jsxs("div", { className: "font-medium flex items-center gap-2", children: [i.name, i.restricted && (_jsx(Badge, { variant: "secondary", title: "Alleen voor deze klant beschikbaar", children: "Exclusief" })), i.floriday_source_label && (_jsx(Badge, { variant: i.floriday_source === "contract" ? "default" : "secondary", children: i.floriday_source_label }))] }), _jsx("div", { className: "text-xs text-muted-foreground", children: [
68
+ i.supplier_article_code,
69
+ i.vbn_code ? `VBN ${i.vbn_code}` : null,
70
+ i.botanical_name,
71
+ i.country_of_origin,
72
+ ].filter(Boolean).join(" · ") || "—" })] }), _jsxs(TableCell, { className: "text-sm text-muted-foreground", children: [_jsx("div", { children: i.packaging ?? "—" }), packing.length > 0 && (_jsx("div", { className: "text-xs", children: packing.join(" · ") }))] }), _jsx(TableCell, { className: "text-right whitespace-nowrap tabular-nums text-muted-foreground", children: money(i.base_price, i.currency) }), _jsxs(TableCell, { className: "text-right whitespace-nowrap tabular-nums", children: [_jsx("div", { className: "font-medium", children: money(i.price, i.currency) }), i.base_price_source && (_jsx("div", { className: "text-[10px] text-muted-foreground", children: i.base_price_source }))] })] }, i.key));
73
+ }) })] }) }) })] }));
74
+ }
@@ -0,0 +1,5 @@
1
+ export declare function FloridayPriceImportDialog({ customerKey, organizationId, }: {
2
+ customerKey: string;
3
+ organizationId?: string | null;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=FloridayPriceImportDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FloridayPriceImportDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/FloridayPriceImportDialog.tsx"],"names":[],"mappings":"AAyBA,wBAAgB,yBAAyB,CAAC,EACxC,WAAW,EACX,cAAc,GACf,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,+BA4IA"}
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
6
+ import { Download } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { applyFloridayPriceImport, previewFloridayPriceImport, } from "../../lib/floriday-price-import.functions";
9
+ import { decodeDeep } from "../../lib/decode-html";
10
+ import { friendlyError } from "../../lib/errors";
11
+ function money(value, currency) {
12
+ if (value == null)
13
+ return "—";
14
+ try {
15
+ return new Intl.NumberFormat("nl-NL", { style: "currency", currency }).format(value);
16
+ }
17
+ catch {
18
+ return `${currency} ${value.toFixed(2)}`;
19
+ }
20
+ }
21
+ export function FloridayPriceImportDialog({ customerKey, organizationId, }) {
22
+ const [open, setOpen] = useState(false);
23
+ const qc = useQueryClient();
24
+ const preview = useServerFn(previewFloridayPriceImport);
25
+ const apply = useServerFn(applyFloridayPriceImport);
26
+ const { data, isLoading, error } = useQuery({
27
+ queryKey: ["floriday-price-import", customerKey, organizationId ?? null],
28
+ queryFn: () => preview({ data: { customerKey, organizationId: organizationId ?? null } }).then(decodeDeep),
29
+ enabled: open,
30
+ });
31
+ const run = useMutation({
32
+ mutationFn: () => apply({ data: { customerKey, organizationId: organizationId ?? null } }),
33
+ onSuccess: (r) => {
34
+ toast.success(r.imported === 0
35
+ ? "Alle prijzen stonden al goed — niets gewijzigd."
36
+ : `${r.imported} prijs(zen) overgenomen uit Floriday.`);
37
+ qc.invalidateQueries({ queryKey: ["customer-assortment"] });
38
+ qc.invalidateQueries({ queryKey: ["floriday-price-import"] });
39
+ setOpen(false);
40
+ },
41
+ onError: (e) => toast.error(friendlyError(e)),
42
+ });
43
+ const rows = data?.rows ?? [];
44
+ const currency = data?.priceGroup?.currency ?? "EUR";
45
+ const changes = (data?.summary.new ?? 0) + (data?.summary.update ?? 0);
46
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setOpen(true), children: [_jsx(Download, { className: "h-4 w-4 mr-2" }), "Prijzen importeren uit Floriday"] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Prijzen importeren uit Floriday" }) }), isLoading ? (_jsx("p", { className: "py-6 text-sm text-muted-foreground", children: "Prijzen ophalen\u2026" })) : error ? (_jsx("p", { className: "py-6 text-sm text-destructive", children: friendlyError(error) })) : !data?.priceGroup ? (_jsx("p", { className: "py-6 text-sm", children: "Deze klant heeft nog geen prijsgroep. Kies eerst een prijsgroep bij de klant; de ge\u00EFmporteerde prijzen worden daarin opgeslagen." })) : rows.length === 0 ? (_jsx("p", { className: "py-6 text-sm", children: "Er staan nog geen Floriday-prijzen klaar voor deze klant. Zodra de catalogusprijzen uit Floriday zijn binnengehaald, verschijnen ze hier automatisch." })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Prijsgroep ", _jsx("strong", { className: "text-foreground", children: data.priceGroup.name }), " \u00B7", " ", data.summary.new, " nieuw \u00B7 ", data.summary.update, " bijgewerkt \u00B7", " ", data.summary.unchanged, " ongewijzigd"] }), _jsx("div", { className: "max-h-[45vh] overflow-auto rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { className: "text-right", children: "Huidig" }), _jsx(TableHead, { className: "text-right", children: "Floriday" }), _jsx(TableHead, { children: "Actie" })] }) }), _jsx(TableBody, { children: rows.map((r) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: r.name }), r.supplier_article_code && (_jsx("div", { className: "text-xs text-muted-foreground", children: r.supplier_article_code }))] }), _jsx(TableCell, { className: "text-xs text-muted-foreground", children: r.source_label }), _jsx(TableCell, { className: "text-right tabular-nums text-muted-foreground", children: money(r.current_price, currency) }), _jsx(TableCell, { className: "text-right tabular-nums font-medium", children: money(r.floriday_price, r.currency) }), _jsx(TableCell, { children: _jsx(Badge, { variant: r.action === "new"
47
+ ? "default"
48
+ : r.action === "update"
49
+ ? "secondary"
50
+ : "outline", children: r.action === "new"
51
+ ? "Nieuw"
52
+ : r.action === "update"
53
+ ? "Bijwerken"
54
+ : "Ongewijzigd" }) })] }, r.trade_item_id))) })] }) })] })), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(false), children: "Annuleren" }), _jsx(Button, { type: "button", onClick: () => run.mutate(), disabled: run.isPending || !data?.priceGroup || changes === 0, children: run.isPending ? "Bezig…" : `Overnemen (${changes})` })] })] }) })] }));
55
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA2TpE"}
1
+ {"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA2TpE"}
@@ -11,9 +11,10 @@ import { ProfileCard, KpiStrip, BadgeList, EmptyState, adaptGroupedToProfile, }
11
11
  import { countryNameToFlag } from "../../lib/countries";
12
12
  import { DiscountsTab } from "../../components/klanten/DiscountsTab";
13
13
  import { PriceGroupBadges } from "../../components/klanten/PriceGroupBadges";
14
+ import { CustomerPriceGroupSelect } from "../../components/klanten/pricegroups/CustomerPriceGroupSelect";
14
15
  import { listPriceGroupsForOrganizations } from "../../lib/price-groups.functions";
15
16
  import { FloridayDeeplinkButton } from "../../components/klanten/FloridayDeeplinkButton";
16
- import { FloridayAssortmentTab } from "../../components/klanten/FloridayAssortmentTab";
17
+ import { CustomerAssortmentTab } from "../../components/klanten/CustomerAssortmentTab";
17
18
  import { decodeDeep } from "../../lib/decode-html";
18
19
  import { DeliveryLocationsTab } from "../../components/klanten/DeliveryLocationsTab";
19
20
  import { KlantBewerkenDialog } from "../../components/klanten/KlantBewerkenDialog";
@@ -90,12 +91,12 @@ export function GroupedKlantDetail({ groupKey }) {
90
91
  provider: providerMap.get(s.provider)?.label ?? s.provider,
91
92
  }))
92
93
  .sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
93
- return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : twinfieldCode ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: localCustomerId ?? null, twinfieldCode: localCustomerId ? null : twinfieldCode, onSaved: () => qc.invalidateQueries() }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
94
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : twinfieldCode ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: localCustomerId ?? null, twinfieldCode: localCustomerId ? null : twinfieldCode, onSaved: () => qc.invalidateQueries() }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx(CustomerPriceGroupSelect, { customerKey: groupKey }), priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Floriday-prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] }))] }), _jsx(KpiStrip, { counts: {
94
95
  contacts: contacts.length,
95
96
  locations: locations.length,
96
97
  assortment: 0,
97
98
  orders: 0,
98
- }, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [(segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Afleverlocaties (logistiek)"] }), _jsx(DeliveryLocationsTab, { groupKey: groupKey, customerId: localCustomerId })] }), locations.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Boekhoudlocaties uit de koppeling (read-only)"] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsxs(TableCell, { className: "text-sm", children: [l.postal_code ? _jsx("span", { className: "font-mono mr-1", children: l.postal_code }) : null, l.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(l.country) ?? "🌐" }), _jsx("span", { children: l.country })] })) : null, !l.postal_code && !l.country ? "—" : null] }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })] }))] }) }), _jsx(TabsContent, { value: "assortment", children: floridayBuyerId ? (_jsx(FloridayAssortmentTab, { organizationId: floridayBuyerId })) : (_jsx(EmptyState, { message: "Assortiment is beschikbaar zodra de koppeling dit levert." })) }), _jsx(TabsContent, { value: "orders", children: _jsx(EmptyState, { message: "Bestellingen zijn beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "trade", children: _jsx(EmptyState, { message: "Handelsinstellingen zijn niet beschikbaar via deze koppeling." }) }), floridayBuyerId && (_jsx(TabsContent, { value: "discounts", children: _jsx(DiscountsTab, { buyerOrganizationId: floridayBuyerId }) })), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: activity.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (activity.map((a) => (_jsxs("div", { className: "flex items-center justify-between text-sm border-l-2 border-primary pl-3 py-1", children: [_jsxs("span", { children: ["Bijgewerkt via ", _jsx("strong", { children: a.provider })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(a.created_at).toLocaleString("nl-NL") })] }, a.id)))) }) }) }), g.is_merged && sources.length > 1 && (_jsx(TabsContent, { value: "sources", children: _jsx(CompareCard, { sources: sources, providerMap: providerMap }) }))] })] })] })] }));
99
+ }, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [(segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Afleverlocaties (logistiek)"] }), _jsx(DeliveryLocationsTab, { groupKey: groupKey, customerId: localCustomerId })] }), locations.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Boekhoudlocaties uit de koppeling (read-only)"] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsxs(TableCell, { className: "text-sm", children: [l.postal_code ? _jsx("span", { className: "font-mono mr-1", children: l.postal_code }) : null, l.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(l.country) ?? "🌐" }), _jsx("span", { children: l.country })] })) : null, !l.postal_code && !l.country ? "—" : null] }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })] }))] }) }), _jsx(TabsContent, { value: "assortment", children: _jsx(CustomerAssortmentTab, { customerKey: groupKey, organizationId: floridayBuyerId }) }), _jsx(TabsContent, { value: "orders", children: _jsx(EmptyState, { message: "Bestellingen zijn beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "trade", children: _jsx(EmptyState, { message: "Handelsinstellingen zijn niet beschikbaar via deze koppeling." }) }), floridayBuyerId && (_jsx(TabsContent, { value: "discounts", children: _jsx(DiscountsTab, { buyerOrganizationId: floridayBuyerId }) })), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: activity.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (activity.map((a) => (_jsxs("div", { className: "flex items-center justify-between text-sm border-l-2 border-primary pl-3 py-1", children: [_jsxs("span", { children: ["Bijgewerkt via ", _jsx("strong", { children: a.provider })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(a.created_at).toLocaleString("nl-NL") })] }, a.id)))) }) }) }), g.is_merged && sources.length > 1 && (_jsx(TabsContent, { value: "sources", children: _jsx(CompareCard, { sources: sources, providerMap: providerMap }) }))] })] })] })] }));
99
100
  }
100
101
  function collectContacts(sources, providerMap) {
101
102
  const out = [];
@@ -1 +1 @@
1
- {"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BA0TjD"}
1
+ {"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAsTjD"}
@@ -12,8 +12,11 @@ import { BackLink } from "../../components/klanten/BackLink";
12
12
  import { CustomerAnalyticsTab } from "../../components/klanten/CustomerAnalyticsTab";
13
13
  import { LocalDiscountsTab } from "../../components/klanten/LocalDiscountsTab";
14
14
  import { PriceGroupBadges } from "../../components/klanten/PriceGroupBadges";
15
+ import { CustomerPriceGroupSelect } from "../../components/klanten/pricegroups/CustomerPriceGroupSelect";
16
+ import { resolveCustomerKeyForLocalCustomer } from "../../lib/price-groups-local.functions";
15
17
  import { FloridayDeeplinkButton } from "../../components/klanten/FloridayDeeplinkButton";
16
18
  import { FloridayAssortmentTab } from "../../components/klanten/FloridayAssortmentTab";
19
+ import { CustomerAssortmentTab } from "../../components/klanten/CustomerAssortmentTab";
17
20
  import { SyncStatusChip } from "../../components/klanten/SyncStatusChip";
18
21
  import { decodeDeep } from "../../lib/decode-html";
19
22
  import { getMissingTwinfieldFields } from "../../lib/twinfield-required-fields";
@@ -34,6 +37,11 @@ export function KlantDetail({ id }) {
34
37
  queryKey: ["customer", id],
35
38
  queryFn: () => get({ data: { id } }).then(decodeDeep),
36
39
  });
40
+ const resolveKey = useServerFn(resolveCustomerKeyForLocalCustomer);
41
+ const { data: keyData } = useQuery({
42
+ queryKey: ["customer-group-key", id],
43
+ queryFn: () => resolveKey({ data: { customerId: id } }),
44
+ });
37
45
  const { data: noteData, isLoading: noteLoading } = useQuery({
38
46
  queryKey: ["customer-note", "customer_id", id],
39
47
  queryFn: () => getNote({ data: { customer_id: id } }),
@@ -71,10 +79,10 @@ export function KlantDetail({ id }) {
71
79
  const c = data.customer;
72
80
  const missingTwinfield = getMissingTwinfieldFields(c);
73
81
  const priceGroups = data.price_groups ?? c.price_groups ?? [];
74
- return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
82
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [keyData?.customerKey ? (_jsx(CustomerPriceGroupSelect, { customerKey: keyData.customerKey })) : null, priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Floriday-prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] }))] }), _jsx(KpiStrip, { counts: {
75
83
  contacts: data.contacts.length,
76
84
  locations: data.delivery_locations.length,
77
85
  assortment: data.assortment.length,
78
86
  orders: data.orders.length,
79
- }, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex flex-wrap h-auto gap-1 mb-4", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsx(TabsTrigger, { value: "dashboard", children: "Analyses" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", data.contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", data.delivery_locations.length, ")"] }), _jsxs(TabsTrigger, { value: "assortment", children: ["Assortiment (", data.assortment.length, ")"] }), c.floriday_organization_id && (_jsx(TabsTrigger, { value: "floriday-assortment", children: "Floriday-aanbod" })), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" })] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [c.bio && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bio" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.bio })] })), (c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Profiel" }) }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: c.segments }), _jsx(BadgeList, { label: "Handelsvorm", items: c.trade_forms }), _jsx(BadgeList, { label: "Afzetmarkten", items: c.markets }), _jsx(BadgeList, { label: "Productgroepen", items: c.product_groups })] })] })) : null, (c.payment_methods?.length ?? 0) > 0 && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Financi\u00EBle afhandeling" }) }), _jsx(CardContent, { children: _jsx(BadgeList, { label: "Betaalmethoden", items: c.payment_methods }) })] })), !c.bio && !(c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) && !(c.payment_methods?.length) && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: data.contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen gesynchroniseerd." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: data.contacts.map((p) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "secondary", children: "Inactief" })] }) }, p.id))) })) }), _jsx(TabsContent, { value: "trade", children: data.trade_settings.length === 0 ? (_jsx(EmptyState, { message: "Handelsinstellingen zijn (nog) niet beschikbaar via de Suppliers API voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.trade_settings.map((s) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: s.is_active ? "default" : "secondary", children: s.is_active ? "Actief" : "Niet actief" }), _jsx("span", { className: "text-sm", children: s.setting_label })] }, s.id))) }) })) }), _jsx(TabsContent, { value: "locations", children: _jsx(DeliveryLocationsTab, { customerId: id }) }), _jsx(TabsContent, { value: "dashboard", children: _jsx(CustomerAnalyticsTab, { customerId: id }) }), _jsx(TabsContent, { value: "assortment", children: data.assortment.length === 0 ? (_jsx(EmptyState, { message: "Geen geselecteerd assortiment voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikelnummer" }), _jsx(TableHead, { children: "Product" }), _jsx(TableHead, { children: "Groep" }), _jsx(TableHead, { children: "Prijs" })] }) }), _jsx(TableBody, { children: data.assortment.map((a) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: a.article_number || "—" }), _jsx(TableCell, { children: a.product_name }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: a.product_group || "—" }), _jsx(TableCell, { children: a.price != null ? `€ ${Number(a.price).toFixed(2)}${a.unit ? ` / ${a.unit}` : ""}` : "—" })] }, a.id))) })] }) }) })) }), c.floriday_organization_id && (_jsx(TabsContent, { value: "floriday-assortment", children: _jsx(FloridayAssortmentTab, { organizationId: c.floriday_organization_id }) })), _jsx(TabsContent, { value: "orders", children: _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Totaal" })] }) }), _jsx(TableBody, { children: data.orders.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center py-6 text-muted-foreground", children: "Geen bestellingen" }) })) : data.orders.map((o) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: o.order_number }), _jsx(TableCell, { children: o.order_date }), _jsx(TableCell, { children: _jsx(Badge, { children: o.status }) }), _jsx(TableCell, { children: o.total })] }, o.id))) })] }) }) }) }), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.audit.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (data.audit.map((a) => _jsx(ActivityItem, { entry: a }, a.id))) }) }) }), _jsx(TabsContent, { value: "discounts", children: _jsx(LocalDiscountsTab, { customerId: id }) })] })] })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: id, onSaved: () => qc.invalidateQueries({ queryKey: ["customer", id] }) })] }));
87
+ }, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex flex-wrap h-auto gap-1 mb-4", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsx(TabsTrigger, { value: "dashboard", children: "Analyses" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", data.contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", data.delivery_locations.length, ")"] }), _jsxs(TabsTrigger, { value: "assortment", children: ["Assortiment (", data.assortment.length, ")"] }), c.floriday_organization_id && (_jsx(TabsTrigger, { value: "floriday-assortment", children: "Floriday-aanbod" })), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" })] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [c.bio && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bio" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.bio })] })), (c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Profiel" }) }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: c.segments }), _jsx(BadgeList, { label: "Handelsvorm", items: c.trade_forms }), _jsx(BadgeList, { label: "Afzetmarkten", items: c.markets }), _jsx(BadgeList, { label: "Productgroepen", items: c.product_groups })] })] })) : null, (c.payment_methods?.length ?? 0) > 0 && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Financi\u00EBle afhandeling" }) }), _jsx(CardContent, { children: _jsx(BadgeList, { label: "Betaalmethoden", items: c.payment_methods }) })] })), !c.bio && !(c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) && !(c.payment_methods?.length) && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: data.contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen gesynchroniseerd." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: data.contacts.map((p) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "secondary", children: "Inactief" })] }) }, p.id))) })) }), _jsx(TabsContent, { value: "trade", children: data.trade_settings.length === 0 ? (_jsx(EmptyState, { message: "Handelsinstellingen zijn (nog) niet beschikbaar via de Suppliers API voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.trade_settings.map((s) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: s.is_active ? "default" : "secondary", children: s.is_active ? "Actief" : "Niet actief" }), _jsx("span", { className: "text-sm", children: s.setting_label })] }, s.id))) }) })) }), _jsx(TabsContent, { value: "locations", children: _jsx(DeliveryLocationsTab, { customerId: id }) }), _jsx(TabsContent, { value: "dashboard", children: _jsx(CustomerAnalyticsTab, { customerId: id }) }), _jsx(TabsContent, { value: "assortment", children: keyData?.customerKey ? (_jsx(CustomerAssortmentTab, { customerKey: keyData.customerKey, organizationId: c.floriday_organization_id ?? null })) : (_jsx(EmptyState, { message: "Assortiment is beschikbaar zodra deze klant gekoppeld is." })) }), c.floriday_organization_id && (_jsx(TabsContent, { value: "floriday-assortment", children: _jsx(FloridayAssortmentTab, { organizationId: c.floriday_organization_id }) })), _jsx(TabsContent, { value: "orders", children: _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Totaal" })] }) }), _jsx(TableBody, { children: data.orders.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center py-6 text-muted-foreground", children: "Geen bestellingen" }) })) : data.orders.map((o) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: o.order_number }), _jsx(TableCell, { children: o.order_date }), _jsx(TableCell, { children: _jsx(Badge, { children: o.status }) }), _jsx(TableCell, { children: o.total })] }, o.id))) })] }) }) }) }), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.audit.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (data.audit.map((a) => _jsx(ActivityItem, { entry: a }, a.id))) }) }) }), _jsx(TabsContent, { value: "discounts", children: _jsx(LocalDiscountsTab, { customerId: id }) })] })] })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: id, onSaved: () => qc.invalidateQueries({ queryKey: ["customer", id] }) })] }));
80
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,gBAAgB,gCAkvB/B"}
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AA0DA,wBAAgB,gBAAgB,gCAgyB/B"}
@@ -13,6 +13,7 @@ import { listOverdueBalances } from "../../lib/overdue-invoices.functions";
13
13
  import { listRevenueActivity } from "../../lib/customer-revenue.functions";
14
14
  import { supabase } from "../../integrations/supabase/client";
15
15
  import { decodeDeep } from "../../lib/decode-html";
16
+ import { friendlyError, isBackendUnavailable, retryOnTransient, retryBackoff } from "../../lib/errors";
16
17
  import { CustomerAvatar } from "../CustomerAvatar";
17
18
  import { PriceGroupBadges } from "./PriceGroupBadges";
18
19
  import { KlantBewerkenDialog } from "./KlantBewerkenDialog";
@@ -65,29 +66,40 @@ export function KlantenOverzicht() {
65
66
  queryKey: ["customer-classes"],
66
67
  queryFn: () => classesFn({ data: undefined }).then(decodeDeep),
67
68
  staleTime: 5 * 60000,
69
+ retry: retryOnTransient,
70
+ retryDelay: retryBackoff,
68
71
  });
69
- const { data: localData, isLoading: loadingLocal } = useQuery({
72
+ const { data: localData, isLoading: loadingLocal, error: localError } = useQuery({
70
73
  queryKey: ["customers-local"],
71
74
  queryFn: () => list({
72
75
  data: { search: "", customer_class: "", country: "", is_active: "all", source: "local" },
73
76
  }).then(decodeDeep),
74
77
  staleTime: 60000,
78
+ retry: retryOnTransient,
79
+ retryDelay: retryBackoff,
75
80
  });
76
- const { data: externalData, isLoading: loadingExternal } = useQuery({
81
+ const { data: externalData, isLoading: loadingExternal, error: externalError } = useQuery({
77
82
  queryKey: ["customers-external"],
78
83
  queryFn: () => listExternal({ data: { status: "all" } }).then(decodeDeep),
79
84
  staleTime: 60000,
85
+ retry: retryOnTransient,
86
+ retryDelay: retryBackoff,
80
87
  });
81
88
  const { data: overdueData } = useQuery({
82
89
  queryKey: ["overdue-balances"],
83
90
  queryFn: () => overdueFn().then(decodeDeep),
84
91
  staleTime: 60000,
92
+ retry: retryOnTransient,
93
+ retryDelay: retryBackoff,
85
94
  });
86
95
  const { data: revenueData, isLoading: loadingRevenue } = useQuery({
87
96
  queryKey: ["customer-revenue-12m"],
88
97
  queryFn: () => revenueFn().then(decodeDeep),
89
98
  staleTime: 60000,
99
+ retry: retryOnTransient,
100
+ retryDelay: retryBackoff,
90
101
  });
102
+ const loadError = (localError ?? externalError);
91
103
  // Realtime: refetch de klantenlijst zodra de Twinfield/Floriday cache verandert.
92
104
  // De DB-trigger heeft de materialized view al ververst, dus een simpele
93
105
  // invalidatie levert direct verse data. Debounced om bulk-syncs op te vangen.
@@ -365,7 +377,16 @@ export function KlantenOverzicht() {
365
377
  });
366
378
  const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
367
379
  const lastUpdated = externalData?.last_fetched_at ?? null;
368
- return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: revenueData
380
+ return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), loadError && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3", children: [_jsx(AlertCircle, { className: "h-5 w-5 text-destructive shrink-0" }), _jsxs("div", { className: "text-sm", children: [_jsx("div", { className: "font-medium", children: isBackendUnavailable(loadError)
381
+ ? "De database is tijdelijk niet bereikbaar"
382
+ : "De klanten konden niet geladen worden" }), _jsx("div", { className: "text-muted-foreground", children: isBackendUnavailable(loadError)
383
+ ? "Je bent niet uitgelogd. Probeer het over een paar minuten opnieuw."
384
+ : friendlyError(loadError) })] }), _jsx(Button, { size: "sm", variant: "outline", className: "ml-auto", onClick: () => {
385
+ queryClient.invalidateQueries({ queryKey: ["customers-local"] });
386
+ queryClient.invalidateQueries({ queryKey: ["customers-external"] });
387
+ queryClient.invalidateQueries({ queryKey: ["overdue-balances"] });
388
+ queryClient.invalidateQueries({ queryKey: ["customer-revenue-12m"] });
389
+ }, children: "Opnieuw proberen" })] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: revenueData
369
390
  ? `Klanten met minimaal één bestelling of factuur sinds ${formatDateNl(revenueData.since)}.\nTotale omzet in die periode: ${formatEuro(revenueData.totals.amount)} · ${revenueData.totals.transactions} transacties`
370
391
  : "Klanten met minimaal één bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: revenueData
371
392
  ? `Klanten zonder enige bestelling of factuur sinds ${formatDateNl(revenueData.since)}.`
@@ -0,0 +1,4 @@
1
+ export declare function CustomerPriceGroupSelect({ customerKey }: {
2
+ customerKey: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=CustomerPriceGroupSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerPriceGroupSelect.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/CustomerPriceGroupSelect.tsx"],"names":[],"mappings":"AAcA,wBAAgB,wBAAwB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,+BA2DhF"}
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { toast } from "@flowselections/core";
6
+ import { SearchableSelect } from "../SearchableSelect";
7
+ import { listCustomerPriceGroups, getPriceGroupsForCustomer, setPriceGroupForCustomer, } from "../../../lib/price-groups-local.functions";
8
+ import { friendlyError } from "../../../lib/errors";
9
+ const NONE = "__none__";
10
+ export function CustomerPriceGroupSelect({ customerKey }) {
11
+ const qc = useQueryClient();
12
+ const listGroups = useServerFn(listCustomerPriceGroups);
13
+ const getCurrent = useServerFn(getPriceGroupsForCustomer);
14
+ const setGroup = useServerFn(setPriceGroupForCustomer);
15
+ const [pending, setPending] = useState(null);
16
+ const { data: groups } = useQuery({
17
+ queryKey: ["price-groups-local"],
18
+ queryFn: () => listGroups(),
19
+ });
20
+ const { data: current } = useQuery({
21
+ queryKey: ["customer-price-group-for-customer", customerKey],
22
+ queryFn: () => getCurrent({ data: { customerKey } }),
23
+ enabled: !!customerKey,
24
+ });
25
+ const options = useMemo(() => [
26
+ { value: NONE, label: "Geen prijsgroep" },
27
+ ...(groups ?? [])
28
+ .filter((g) => g.is_active !== false)
29
+ .map((g) => ({ value: g.id, label: g.name })),
30
+ ], [groups]);
31
+ const mutation = useMutation({
32
+ mutationFn: (priceGroupId) => setGroup({ data: { customerKey, priceGroupId } }),
33
+ onSuccess: () => {
34
+ toast.success("Prijsgroep bijgewerkt");
35
+ qc.invalidateQueries({ queryKey: ["customer-price-group-for-customer", customerKey] });
36
+ qc.invalidateQueries({ queryKey: ["price-groups-local"] });
37
+ qc.invalidateQueries({ queryKey: ["customer-price-group"] });
38
+ },
39
+ onError: (err) => toast.error(friendlyError(err)),
40
+ onSettled: () => setPending(null),
41
+ });
42
+ const value = pending ?? current?.priceGroupIds?.[0] ?? NONE;
43
+ return (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijsgroep:" }), _jsx("div", { className: "w-[240px]", children: _jsx(SearchableSelect, { value: value, onValueChange: (v) => {
44
+ setPending(v);
45
+ mutation.mutate(v === NONE ? null : v);
46
+ }, options: options, placeholder: "Geen prijsgroep" }) })] }));
47
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"PriceGroupFormPage.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupFormPage.tsx"],"names":[],"mappings":"AAoLA,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BAusB9E"}
1
+ {"version":3,"file":"PriceGroupFormPage.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupFormPage.tsx"],"names":[],"mappings":"AAqLA,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BA2sB9E"}
@@ -10,6 +10,7 @@ import { createCustomerPriceGroup, updateCustomerPriceGroup, getCustomerPriceGro
10
10
  import { computePrice } from "../../../lib/pricing";
11
11
  import { listTradeItemsForPreview } from "../../../lib/floriday-trade-items.functions";
12
12
  import { ProductPickerDialog } from "./ProductPickerDialog";
13
+ import { PriceGroupItemPrices } from "./PriceGroupItemPrices";
13
14
  import { CustomerPickerDialog } from "./CustomerPickerDialog";
14
15
  import { UserPlus, Users2 } from "lucide-react";
15
16
  import { listExternalCustomers } from "../../../lib/external-customers.functions";
@@ -256,7 +257,7 @@ export function PriceGroupFormPage({ editingId }) {
256
257
  ] }))] }), _jsxs("div", { className: "pt-2", children: [_jsx("div", { className: "text-sm text-muted-foreground mb-2", children: "Diensten" }), _jsx(Separator, { className: "mb-3" }), _jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none", children: [_jsx(Checkbox, { checked: form.include_stickers, onCheckedChange: (v) => patch({ include_stickers: v === true }) }), _jsx("span", { className: "text-sm font-medium", children: "Inclusief stickeren" })] })] }), _jsxs("div", { className: "pt-2", children: [_jsx("div", { className: "text-sm text-muted-foreground mb-2", children: "Afname" }), _jsx(Separator, { className: "mb-3" }), _jsxs("div", { className: "max-w-sm", children: [_jsx(Label, { children: "Afname-eenheid" }), _jsxs(Select, { value: form.unit, onValueChange: (v) => patch({ unit: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "- Selecteer een eenheid -" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "per_fust", children: "Per fust" }), _jsx(SelectItem, { value: "per_laag", children: "Per laag" }), _jsx(SelectItem, { value: "per_ladingdrager", children: "Per ladingdrager" })] })] })] }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2 leading-relaxed", children: ["Afname-eenheid per aangeboden artikel.", _jsx("br", {}), "Deze instelling geldt voor alle klanten in deze prijsgroep, per aangeboden artikel, niet per bestelling."] })] }), _jsx("div", { className: "pt-2 flex justify-end", children: _jsxs(Button, { onClick: () => nextStep(1), children: ["Volgende stap ", _jsx(ArrowRight, { className: "h-4 w-4 ml-1" })] }) })] }) }), _jsx(StepSection, { index: 2, title: STEP_TITLES[2], subtitle: summaries[2], open: openStep === 2, completed: form.pricing_mode === "calculated" && maxReached >= 2 && openStep !== 2, disabled: maxReached < 2 || form.pricing_mode !== "calculated", onToggle: () => goToStep(2), children: _jsx("div", { className: "space-y-4", children: form.pricing_mode !== "calculated" ? (_jsx("div", { className: "text-sm text-muted-foreground p-4 border rounded-md bg-muted/30", children: "Deze prijsgroep gebruikt handmatige prijzen. Prijsberekening is niet van toepassing." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx(Label, { children: "Korting/opslag per stuk" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-3 mt-1.5", children: [_jsxs(Select, { value: form.calc_type ?? "discount", onValueChange: (v) => patch({ calc_type: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "discount", children: "Korting" }), _jsx(SelectItem, { value: "surcharge", children: "Opslag" })] })] }), _jsxs(Select, { value: form.calc_value_type ?? "fixed", onValueChange: (v) => patch({ calc_value_type: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "percentage", children: "Percentage" }), _jsx(SelectItem, { value: "fixed", children: "Vast bedrag" })] })] }), _jsxs("div", { className: "relative", children: [form.calc_value_type !== "percentage" && (_jsx("span", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none", children: "\u20AC" })), _jsx(Input, { type: "number", step: form.calc_value_type === "percentage" ? "1" : "0.001", value: form.calc_value ?? 0, onChange: (e) => patch({ calc_value: Number(e.target.value) }), className: form.calc_value_type === "percentage" ? "pr-8" : "pl-7" }), form.calc_value_type === "percentage" && (_jsx("span", { className: "absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none", children: "%" }))] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Afhandelingskosten (optioneel)", _jsx(Info, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-label": "Extra kosten die bovenop de prijs komen. Kies hoe deze verdeeld worden." })] }), _jsxs("div", { className: "relative mt-1.5", children: [_jsx("span", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none", children: "\u20AC" }), _jsx(Input, { type: "number", step: "0.01", value: form.handling_fee ?? 0, onChange: (e) => patch({ handling_fee: Number(e.target.value) }), className: "pl-7" })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Verdeeld over het aantal planten of bloemen op een" }), _jsxs(Select, { value: form.distribute_over ?? "ladingdrager", onValueChange: (v) => patch({ distribute_over: v }), children: [_jsx(SelectTrigger, { className: "mt-1.5", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "ladingdrager", children: "Ladingdrager" }), _jsx(SelectItem, { value: "laag", children: "Laag" }), _jsx(SelectItem, { value: "fust", children: "Fust" })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx(Label, { children: "Prijs afronden" }), _jsxs(Select, { value: form.rounding, onValueChange: (v) => patch({ rounding: v }), children: [_jsx(SelectTrigger, { className: "mt-1.5", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "none", children: "Niet afronden" }), _jsx(SelectItem, { value: "up", children: "Omhoog" }), _jsx(SelectItem, { value: "down", children: "Omlaag" }), _jsx(SelectItem, { value: "nearest", children: "Dichtstbijzijnde" })] })] })] }), form.rounding !== "none" && (_jsxs("div", { children: [_jsx(Label, { children: "Op" }), _jsxs(Select, { value: String(form.rounding_step ?? 0.05), onValueChange: (v) => patch({ rounding_step: Number(v) }), children: [_jsx(SelectTrigger, { className: "mt-1.5", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "0.01", children: "\u20AC 0,01" }), _jsx(SelectItem, { value: "0.05", children: "\u20AC 0,05" }), _jsx(SelectItem, { value: "0.1", children: "\u20AC 0,10" })] })] })] }))] }), _jsx("div", { className: "pt-2 flex justify-end", children: _jsxs(Button, { onClick: () => nextStep(2), children: ["Volgende stap ", _jsx(ArrowRight, { className: "h-4 w-4 ml-1" })] }) })] })) }) }), _jsxs(StepSection, { index: 3, title: STEP_TITLES[3], subtitle: summaries[3], open: openStep === 3, completed: form.group_type === "customer_specific" && maxReached >= 3 && openStep !== 3 && selectedCustomerKeys.length > 0, disabled: maxReached < 3 || form.group_type !== "customer_specific", onToggle: () => goToStep(3), children: [form.group_type !== "customer_specific" ? (_jsx("div", { className: "text-sm text-muted-foreground p-4 border rounded-md bg-muted/30", children: "Algemene prijsgroepen zijn zichtbaar voor alle klanten \u2014 een doelgroep is niet van toepassing." })) : selectedCustomerKeys.length === 0 ? (_jsxs("div", { className: "rounded-lg border bg-muted/20 p-8", children: [_jsxs("div", { className: "flex flex-col items-center text-center gap-3", children: [_jsxs("div", { className: "relative h-16 w-16 rounded-full bg-muted flex items-center justify-center", children: [_jsx(Users2, { className: "h-8 w-8 text-muted-foreground/60" }), _jsx("span", { className: "absolute -top-1 -right-1 h-6 w-6 rounded-full bg-background border flex items-center justify-center", children: _jsx(UserPlus, { className: "h-3.5 w-3.5 text-muted-foreground" }) })] }), _jsx("div", { children: _jsx("div", { className: "font-medium text-sm", children: "Nog geen klanten toegevoegd" }) })] }), _jsx("div", { className: "flex justify-end mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => setCustomerPickerOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-1.5" }), " Organisaties wijzigen"] }) })] })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: [_jsx("strong", { className: "text-foreground", children: selectedCustomerKeys.length }), " ", "klant", selectedCustomerKeys.length === 1 ? "" : "en", " gekoppeld"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setCustomerPickerOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-1.5" }), " Organisaties wijzigen"] })] }), _jsx("div", { className: "max-h-[320px] overflow-y-auto border rounded-md divide-y", children: selectedCustomerKeys.map((k) => {
257
258
  const c = customersByKey.get(k);
258
259
  return (_jsxs("div", { className: "flex items-center gap-3 p-2.5", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium truncate", children: c?.display_name ?? k }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: c ? (c.city || c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [c.city && _jsx("span", { children: c.city }), c.city && c.country && _jsx("span", { children: "\u00B7" }), c.country && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: c.country })] }))] })) : "—") : "Niet in cache" })] }), _jsx("div", { className: "flex gap-1", children: (c?.providers ?? []).slice(0, 3).map((p) => (_jsx(Badge, { variant: "secondary", className: "text-[10px]", children: p }, p))) })] }, k));
259
- }) })] })), _jsx(CustomerPickerDialog, { open: customerPickerOpen, onOpenChange: setCustomerPickerOpen, value: selectedCustomerKeys, onChange: setSelectedCustomerKeys })] }), error && _jsx("div", { className: "text-sm text-destructive", children: error })] }), form.pricing_mode === "calculated" && (_jsxs("aside", { className: "lg:sticky lg:top-4 space-y-3", children: [_jsx(Card, { children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm font-medium", children: "Voorbeeld prijscalculatie" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(-1), disabled: items.length < 2, "aria-label": "Vorige product", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(1), disabled: items.length < 2, "aria-label": "Volgend product", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }), _jsxs("button", { type: "button", onClick: () => setPickerOpen(true), className: "w-full flex items-center gap-3 text-left hover:bg-muted/30 rounded-md p-2 -m-2", children: [_jsx("div", { className: "h-14 w-14 rounded bg-muted flex items-center justify-center overflow-hidden shrink-0", children: selectedItem?.image_url
260
+ }) })] })), _jsx(CustomerPickerDialog, { open: customerPickerOpen, onOpenChange: setCustomerPickerOpen, value: selectedCustomerKeys, onChange: setSelectedCustomerKeys })] }), editingId && (_jsx(PriceGroupItemPrices, { priceGroupId: editingId, currency: form.currency })), error && _jsx("div", { className: "text-sm text-destructive", children: error })] }), form.pricing_mode === "calculated" && (_jsxs("aside", { className: "lg:sticky lg:top-4 space-y-3", children: [_jsx(Card, { children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm font-medium", children: "Voorbeeld prijscalculatie" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(-1), disabled: items.length < 2, "aria-label": "Vorige product", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(1), disabled: items.length < 2, "aria-label": "Volgend product", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }), _jsxs("button", { type: "button", onClick: () => setPickerOpen(true), className: "w-full flex items-center gap-3 text-left hover:bg-muted/30 rounded-md p-2 -m-2", children: [_jsx("div", { className: "h-14 w-14 rounded bg-muted flex items-center justify-center overflow-hidden shrink-0", children: selectedItem?.image_url
260
261
  ? _jsx("img", { src: selectedItem.image_url, alt: "", className: "h-full w-full object-contain" })
261
262
  : _jsx(ImageIcon, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-sm font-medium truncate", children: selectedItem?.name ?? "Geen product beschikbaar" }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: selectedItem
262
263
  ? [selectedItem.packaging_code,
@@ -0,0 +1,5 @@
1
+ export declare function PriceGroupItemPrices({ priceGroupId, currency, }: {
2
+ priceGroupId: string;
3
+ currency?: string;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=PriceGroupItemPrices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PriceGroupItemPrices.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupItemPrices.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,oBAAoB,CAAC,EACnC,YAAY,EACZ,QAAgB,GACjB,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,+BA0HA"}