@flowselections/floriday-klanten-module 1.0.8 → 1.0.10

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 (51) hide show
  1. package/dist-lib/components/klanten/CustomerAnalyticsTab.d.ts +4 -0
  2. package/dist-lib/components/klanten/CustomerAnalyticsTab.d.ts.map +1 -0
  3. package/dist-lib/components/klanten/CustomerAnalyticsTab.js +90 -0
  4. package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts +4 -0
  5. package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts.map +1 -0
  6. package/dist-lib/components/klanten/DeliveryLocationsTab.js +94 -0
  7. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts +4 -0
  8. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -0
  9. package/dist-lib/components/klanten/GroupedKlantDetail.js +152 -0
  10. package/dist-lib/components/klanten/KlantDetail.d.ts +4 -0
  11. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -0
  12. package/dist-lib/components/klanten/KlantDetail.js +43 -0
  13. package/dist-lib/components/klanten/KlantDetailLayout.d.ts +75 -0
  14. package/dist-lib/components/klanten/KlantDetailLayout.d.ts.map +1 -0
  15. package/dist-lib/components/klanten/KlantDetailLayout.js +172 -0
  16. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  17. package/dist-lib/components/klanten/KlantenOverzicht.js +12 -32
  18. package/dist-lib/components/klanten/NetwerkPagina.d.ts +2 -0
  19. package/dist-lib/components/klanten/NetwerkPagina.d.ts.map +1 -0
  20. package/dist-lib/components/klanten/NetwerkPagina.js +23 -0
  21. package/dist-lib/components/klanten/NieuweKlant.d.ts +2 -0
  22. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -0
  23. package/dist-lib/components/klanten/NieuweKlant.js +57 -0
  24. package/dist-lib/index.d.ts +4 -0
  25. package/dist-lib/index.d.ts.map +1 -1
  26. package/dist-lib/index.js +5 -1
  27. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2797 -144
  28. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/client.d.ts +2797 -144
  30. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  31. package/dist-lib/integrations/supabase/client.server.d.ts +2797 -144
  32. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  33. package/dist-lib/integrations/supabase/types.d.ts +2870 -134
  34. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  35. package/dist-lib/integrations/supabase/types.js +2 -0
  36. package/dist-lib/lib/accounting.functions.d.ts +17599 -9640
  37. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  38. package/dist-lib/lib/customer-analytics.functions.d.ts +9347 -0
  39. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -0
  40. package/dist-lib/lib/customer-analytics.functions.js +165 -0
  41. package/dist-lib/lib/customers.functions.d.ts +42763 -24186
  42. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  43. package/dist-lib/lib/delivery-locations.functions.d.ts +37258 -0
  44. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -0
  45. package/dist-lib/lib/delivery-locations.functions.js +150 -0
  46. package/dist-lib/lib/eu-regions.d.ts +14 -0
  47. package/dist-lib/lib/eu-regions.d.ts.map +1 -0
  48. package/dist-lib/lib/eu-regions.js +68 -0
  49. package/dist-lib/lib/external-customers.functions.d.ts +25112 -14500
  50. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  51. package/package.json +5 -4
@@ -0,0 +1,4 @@
1
+ export declare function CustomerAnalyticsTab({ customerId }: {
2
+ customerId: string;
3
+ }): import("react").JSX.Element | null;
4
+ //# sourceMappingURL=CustomerAnalyticsTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerAnalyticsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerAnalyticsTab.tsx"],"names":[],"mappings":"AA0FA,wBAAgB,oBAAoB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,sCA6N1E"}
@@ -0,0 +1,90 @@
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, CardHeader, CardTitle, Badge, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Input, } from "@flowselections/core";
6
+ import { ResponsiveContainer, LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, PieChart, Pie, Cell, Legend, } from "recharts";
7
+ import { TrendingUp, TrendingDown, Minus } from "lucide-react";
8
+ import { getCustomerAnalytics } from "../../lib/customer-analytics.functions";
9
+ function presetRange(key) {
10
+ const now = new Date();
11
+ const y = now.getFullYear(), m = now.getMonth(), d = now.getDate();
12
+ switch (key) {
13
+ case "today": return { from: new Date(y, m, d), to: new Date(y, m, d + 1) };
14
+ case "week": {
15
+ const day = now.getDay() || 7;
16
+ const start = new Date(y, m, d - day + 1);
17
+ return { from: start, to: new Date(start.getTime() + 7 * 86400000) };
18
+ }
19
+ case "month": return { from: new Date(y, m, 1), to: new Date(y, m + 1, 1) };
20
+ case "quarter": {
21
+ const q = Math.floor(m / 3);
22
+ return { from: new Date(y, q * 3, 1), to: new Date(y, q * 3 + 3, 1) };
23
+ }
24
+ case "year": return { from: new Date(y, 0, 1), to: new Date(y + 1, 0, 1) };
25
+ case "last_year": return { from: new Date(y - 1, 0, 1), to: new Date(y, 0, 1) };
26
+ case "all":
27
+ default: return { from: new Date(2000, 0, 1), to: new Date(y + 1, 0, 1) };
28
+ }
29
+ }
30
+ const PRESETS = [
31
+ { key: "today", label: "Vandaag" },
32
+ { key: "week", label: "Deze week" },
33
+ { key: "month", label: "Deze maand" },
34
+ { key: "quarter", label: "Dit kwartaal" },
35
+ { key: "year", label: "Dit jaar" },
36
+ { key: "last_year", label: "Vorig jaar" },
37
+ { key: "all", label: "Alles" },
38
+ { key: "custom", label: "Aangepast" },
39
+ ];
40
+ const PIE_COLORS = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed", "#0891b2", "#db2777", "#65a30d"];
41
+ function formatEUR(n) {
42
+ return new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(n);
43
+ }
44
+ function formatNum(n, d = 0) {
45
+ return new Intl.NumberFormat("nl-NL", { maximumFractionDigits: d }).format(n);
46
+ }
47
+ function formatDate(s) {
48
+ if (!s)
49
+ return "—";
50
+ return new Date(s).toLocaleDateString("nl-NL", { day: "2-digit", month: "short", year: "numeric" });
51
+ }
52
+ function Kpi({ label, value, hint, delta }) {
53
+ let deltaEl = null;
54
+ if (delta != null && isFinite(delta)) {
55
+ const Icon = delta > 0.5 ? TrendingUp : delta < -0.5 ? TrendingDown : Minus;
56
+ const color = delta > 0.5 ? "text-emerald-600" : delta < -0.5 ? "text-rose-600" : "text-muted-foreground";
57
+ deltaEl = (_jsxs("div", { className: `flex items-center gap-1 text-xs ${color}`, children: [_jsx(Icon, { className: "h-3 w-3" }), " ", delta > 0 ? "+" : "", formatNum(delta, 1), "%"] }));
58
+ }
59
+ return (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-4 pb-4 space-y-1", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: "text-xl font-semibold tabular-nums", children: value }), _jsxs("div", { className: "flex items-center justify-between", children: [hint && _jsx("div", { className: "text-xs text-muted-foreground", children: hint }), deltaEl] })] }) }));
60
+ }
61
+ export function CustomerAnalyticsTab({ customerId }) {
62
+ const [preset, setPreset] = useState("year");
63
+ const [customFrom, setCustomFrom] = useState("");
64
+ const [customTo, setCustomTo] = useState("");
65
+ const range = useMemo(() => {
66
+ if (preset === "custom" && customFrom && customTo) {
67
+ return { from: new Date(customFrom), to: new Date(new Date(customTo).getTime() + 86400000) };
68
+ }
69
+ return presetRange(preset === "custom" ? "year" : preset);
70
+ }, [preset, customFrom, customTo]);
71
+ const fetchAnalytics = useServerFn(getCustomerAnalytics);
72
+ const { data, isLoading, error } = useQuery({
73
+ queryKey: ["customer-analytics", customerId, range.from.toISOString(), range.to.toISOString()],
74
+ queryFn: () => fetchAnalytics({ data: {
75
+ customerId, from: range.from.toISOString(), to: range.to.toISOString(),
76
+ } }),
77
+ });
78
+ if (isLoading)
79
+ return _jsx("div", { className: "text-sm text-muted-foreground p-4", children: "Analyses laden..." });
80
+ if (error)
81
+ return _jsxs("div", { className: "text-sm text-rose-600 p-4", children: ["Kon analyses niet laden: ", error.message] });
82
+ if (!data)
83
+ return null;
84
+ const a = data;
85
+ const k = a.kpis;
86
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { children: _jsx(CardContent, { className: "pt-4 pb-4", children: _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [PRESETS.map((p) => (_jsx(Button, { size: "sm", variant: preset === p.key ? "default" : "outline", onClick: () => setPreset(p.key), children: p.label }, p.key))), preset === "custom" && (_jsxs("div", { className: "flex items-center gap-2 ml-2", children: [_jsx(Input, { type: "date", value: customFrom, onChange: (e) => setCustomFrom(e.target.value), className: "h-8 w-40" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "tot" }), _jsx(Input, { type: "date", value: customTo, onChange: (e) => setCustomTo(e.target.value), className: "h-8 w-40" })] }))] }) }) }), _jsxs("div", { className: "grid gap-3 grid-cols-2 md:grid-cols-4", children: [_jsx(Kpi, { label: "Omzet in periode", value: formatEUR(k.revenue_period), delta: k.revenue_growth_pct, hint: `vs vorige periode: ${formatEUR(k.revenue_compare)}` }), _jsx(Kpi, { label: "Aantal bestellingen", value: formatNum(k.order_count_period), hint: `totaal: ${formatNum(k.order_count)}` }), _jsx(Kpi, { label: "Gem. orderwaarde", value: formatEUR(k.avg_order_value) }), _jsx(Kpi, { label: "Totale omzet (alle tijd)", value: formatEUR(k.total_revenue) }), _jsx(Kpi, { label: "Dit jaar", value: formatEUR(k.revenue_this_year), hint: `vorig jaar: ${formatEUR(k.revenue_last_year)}` }), _jsx(Kpi, { label: "Deze maand", value: formatEUR(k.revenue_this_month), hint: `vorige maand: ${formatEUR(k.revenue_last_month)}` }), _jsx(Kpi, { label: "Grootste bestelling", value: formatEUR(k.largest_order), hint: `kleinste: ${formatEUR(k.smallest_order)}` }), _jsx(Kpi, { label: "Gem. tijd tussen bestellingen", value: k.avg_days_between_orders != null ? `${formatNum(k.avg_days_between_orders, 1)} dgn` : "—", hint: k.avg_orders_per_month != null ? `${formatNum(k.avg_orders_per_month, 1)} / maand` : undefined })] }), _jsxs("div", { className: "grid gap-3 grid-cols-1 md:grid-cols-3 text-sm", children: [_jsx(Card, { children: _jsxs(CardContent, { className: "pt-4 pb-4", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Eerste bestelling" }), _jsx("div", { className: "font-medium", children: formatDate(k.first_order_date) })] }) }), _jsx(Card, { children: _jsxs(CardContent, { className: "pt-4 pb-4", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Laatste bestelling" }), _jsx("div", { className: "font-medium", children: formatDate(k.last_order_date) })] }) }), _jsx(Card, { children: _jsxs(CardContent, { className: "pt-4 pb-4", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Periode" }), _jsxs("div", { className: "font-medium", children: [formatDate(a.period.from), " \u2013 ", formatDate(new Date(new Date(a.period.to).getTime() - 86400000).toISOString())] })] }) })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Omzet per maand" }) }), _jsx(CardContent, { className: "h-72", children: a.monthly.length === 0 ? _jsx(EmptyChart, {}) : (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(LineChart, { data: a.monthly, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }), _jsx(XAxis, { dataKey: "label", fontSize: 11 }), _jsx(YAxis, { fontSize: 11, tickFormatter: (v) => formatEUR(Number(v)) }), _jsx(Tooltip, { formatter: (v) => formatEUR(Number(v)) }), _jsx(Line, { type: "monotone", dataKey: "revenue", stroke: "#2563eb", strokeWidth: 2, dot: false, name: "Omzet" })] }) })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Aantal bestellingen per maand" }) }), _jsx(CardContent, { className: "h-72", children: a.monthly.length === 0 ? _jsx(EmptyChart, {}) : (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(BarChart, { data: a.monthly, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }), _jsx(XAxis, { dataKey: "label", fontSize: 11 }), _jsx(YAxis, { fontSize: 11, allowDecimals: false }), _jsx(Tooltip, {}), _jsx(Bar, { dataKey: "orders", fill: "#16a34a", name: "Bestellingen" })] }) })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gem. orderwaarde per maand" }) }), _jsx(CardContent, { className: "h-72", children: a.monthly.length === 0 ? _jsx(EmptyChart, {}) : (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(LineChart, { data: a.monthly, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }), _jsx(XAxis, { dataKey: "label", fontSize: 11 }), _jsx(YAxis, { fontSize: 11, tickFormatter: (v) => formatEUR(Number(v)) }), _jsx(Tooltip, { formatter: (v) => formatEUR(Number(v)) }), _jsx(Line, { type: "monotone", dataKey: "aov", stroke: "#f59e0b", strokeWidth: 2, dot: false, name: "Gem. waarde" })] }) })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Omzet per jaar" }) }), _jsx(CardContent, { className: "h-72", children: a.yearly.length === 0 ? _jsx(EmptyChart, {}) : (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(BarChart, { data: a.yearly, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }), _jsx(XAxis, { dataKey: "label", fontSize: 11 }), _jsx(YAxis, { fontSize: 11, tickFormatter: (v) => formatEUR(Number(v)) }), _jsx(Tooltip, { formatter: (v) => formatEUR(Number(v)) }), _jsx(Bar, { dataKey: "revenue", fill: "#7c3aed", name: "Omzet" })] }) })) })] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-[2fr_1fr]", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Top artikelen in periode" }) }), _jsx(CardContent, { className: "p-0", children: a.topProducts.length === 0 ? (_jsx("div", { className: "p-6 text-sm text-muted-foreground", children: "Geen orderregels in deze periode." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Product" }), _jsx(TableHead, { className: "text-right", children: "Aantal" }), _jsx(TableHead, { children: "Eenheid" }), _jsx(TableHead, { className: "text-right", children: "Omzet" })] }) }), _jsx(TableBody, { children: a.topProducts.map((p) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: p.product_name }), _jsx(TableCell, { className: "text-right tabular-nums", children: formatNum(p.quantity) }), _jsx(TableCell, { className: "text-muted-foreground", children: p.unit ?? "—" }), _jsx(TableCell, { className: "text-right tabular-nums", children: formatEUR(p.revenue) })] }, p.product_name))) })] })) })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Verkocht per eenheid" }) }), _jsx(CardContent, { className: "space-y-2", children: a.unitTotals.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground", children: "Geen data." })) : a.unitTotals.map((u) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "capitalize", children: u.unit }), _jsx("span", { className: "tabular-nums font-medium", children: formatNum(u.quantity) })] }, u.unit))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Status van bestellingen" }) }), _jsx(CardContent, { className: "h-56", children: a.statusBreakdown.length === 0 ? _jsx(EmptyChart, {}) : (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(PieChart, { children: [_jsx(Pie, { data: a.statusBreakdown, dataKey: "count", nameKey: "status", outerRadius: 70, label: true, children: a.statusBreakdown.map((_, i) => (_jsx(Cell, { fill: PIE_COLORS[i % PIE_COLORS.length] }, i))) }), _jsx(Tooltip, {}), _jsx(Legend, {})] }) })) })] })] })] }), _jsx(Card, { children: _jsx(CardContent, { className: "pt-4 pb-4 text-xs text-muted-foreground space-y-1", children: _jsxs("div", { children: [_jsx(Badge, { variant: "secondary", children: "Let op" }), " Sommige analyses zijn nog niet beschikbaar omdat de onderliggende data ontbreekt: btw-bedragen, marge en vervoerder/afhaallocatie per bestelling. Voeg deze velden toe aan orders/orderregels om die overzichten te activeren."] }) }) })] }));
87
+ }
88
+ function EmptyChart() {
89
+ return _jsx("div", { className: "h-full flex items-center justify-center text-sm text-muted-foreground", children: "Geen data in deze periode." });
90
+ }
@@ -0,0 +1,4 @@
1
+ export declare function DeliveryLocationsTab({ customerId }: {
2
+ customerId: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=DeliveryLocationsTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeliveryLocationsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/DeliveryLocationsTab.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,oBAAoB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,+BAwS1E"}
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Button, Card, CardContent, Badge, toast, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@flowselections/core";
6
+ import { MapPin, Plus, Pencil, Trash2, Truck, Sparkles, PackageCheck } from "lucide-react";
7
+ import { listCarriers, listDeliveryLocations, upsertDeliveryLocation, deleteDeliveryLocation, pickCarrierForLocation, } from "../../lib/delivery-locations.functions";
8
+ import { EU_COUNTRIES, findCountry } from "../../lib/eu-regions";
9
+ const EMPTY = {
10
+ name: "", street: "", house_number: "", postal_code: "",
11
+ city: "", province: "", country: "Nederland",
12
+ carrier_id: null, carrier_assignment_source: "auto", is_primary: false, self_pickup: false,
13
+ };
14
+ export function DeliveryLocationsTab({ customerId }) {
15
+ const qc = useQueryClient();
16
+ const listFn = useServerFn(listDeliveryLocations);
17
+ const carriersFn = useServerFn(listCarriers);
18
+ const upsertFn = useServerFn(upsertDeliveryLocation);
19
+ const deleteFn = useServerFn(deleteDeliveryLocation);
20
+ const { data: list } = useQuery({
21
+ queryKey: ["delivery-locations", customerId],
22
+ queryFn: () => listFn({ data: { customer_id: customerId } }),
23
+ });
24
+ const { data: carriersResp } = useQuery({
25
+ queryKey: ["carriers"],
26
+ queryFn: () => carriersFn(),
27
+ });
28
+ const carriers = carriersResp?.carriers ?? [];
29
+ const locations = (list?.locations ?? []);
30
+ const [open, setOpen] = useState(false);
31
+ const [form, setForm] = useState(EMPTY);
32
+ const autoSuggested = useMemo(() => {
33
+ if (form.carrier_assignment_source === "manual")
34
+ return null;
35
+ return pickCarrierForLocation({ country: form.country, province: form.province, city: form.city }, carriers);
36
+ }, [form.country, form.province, form.city, form.carrier_assignment_source, carriers]);
37
+ useEffect(() => {
38
+ if (form.carrier_assignment_source !== "manual" && autoSuggested) {
39
+ setForm((f) => ({ ...f, carrier_id: autoSuggested.carrier_id }));
40
+ }
41
+ }, [autoSuggested?.carrier_id, form.carrier_assignment_source]);
42
+ const carrierName = (id) => carriers.find((c) => c.id === id)?.name ?? "—";
43
+ const startNew = () => { setForm(EMPTY); setOpen(true); };
44
+ const startEdit = (loc) => {
45
+ setForm({
46
+ id: loc.id,
47
+ name: loc.name ?? "",
48
+ street: loc.street ?? "",
49
+ house_number: loc.house_number ?? "",
50
+ postal_code: loc.postal_code ?? "",
51
+ city: loc.city ?? "",
52
+ province: loc.province ?? "",
53
+ country: loc.country ?? "Nederland",
54
+ carrier_id: loc.carrier_id,
55
+ carrier_assignment_source: (loc.carrier_assignment_source ?? "auto"),
56
+ is_primary: loc.is_primary,
57
+ self_pickup: !!loc.self_pickup,
58
+ });
59
+ setOpen(true);
60
+ };
61
+ const saveMut = useMutation({
62
+ mutationFn: () => upsertFn({ data: { ...form, customer_id: customerId } }),
63
+ onSuccess: () => {
64
+ toast.success(form.id ? "Locatie bijgewerkt" : "Locatie toegevoegd");
65
+ qc.invalidateQueries({ queryKey: ["delivery-locations", customerId] });
66
+ qc.invalidateQueries({ queryKey: ["customer", customerId] });
67
+ setOpen(false);
68
+ },
69
+ onError: (e) => toast.error(e?.message ?? "Opslaan mislukt"),
70
+ });
71
+ const delMut = useMutation({
72
+ mutationFn: (id) => deleteFn({ data: { id } }),
73
+ onSuccess: () => {
74
+ toast.success("Locatie verwijderd");
75
+ qc.invalidateQueries({ queryKey: ["delivery-locations", customerId] });
76
+ qc.invalidateQueries({ queryKey: ["customer", customerId] });
77
+ },
78
+ });
79
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: "Voeg afleverlocaties toe; de vervoerder wordt automatisch gekoppeld op basis van het bedieningsgebied." }), _jsxs(Button, { size: "sm", onClick: startNew, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Locatie toevoegen"] })] }), locations.length === 0 ? (_jsx(Card, { children: _jsx(CardContent, { className: "py-10 text-center text-sm text-muted-foreground", children: "Nog geen afleverlocaties." }) })) : (_jsx("div", { className: "grid gap-3 md:grid-cols-2", children: locations.map((loc) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-5 space-y-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-semibold flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), loc.name || [loc.street, loc.house_number].filter(Boolean).join(" ") || "Afleverlocatie"] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-0.5", children: [[loc.street, loc.house_number].filter(Boolean).join(" "), (loc.street || loc.house_number) && (loc.postal_code || loc.city) ? ", " : "", [loc.postal_code, loc.city].filter(Boolean).join(" "), loc.province ? ` · ${loc.province}` : "", loc.country ? ` · ${loc.country}` : ""] })] }), _jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => startEdit(loc), children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", children: _jsx(Trash2, { className: "h-4 w-4" }) }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Locatie verwijderen?" }), _jsx(AlertDialogDescription, { children: "Deze actie kan niet ongedaan worden gemaakt." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(loc.id), children: "Verwijderen" })] })] })] })] })] }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap text-sm", children: [loc.self_pickup ? (_jsxs(_Fragment, { children: [_jsx(PackageCheck, { className: "h-4 w-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: "Eigen vervoer" }), _jsx(Badge, { variant: "secondary", children: "Klant haalt zelf op" })] })) : (_jsxs(_Fragment, { children: [_jsx(Truck, { className: "h-4 w-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: carrierName(loc.carrier_id) }), loc.carrier_assignment_source === "auto" && loc.carrier_id && (_jsxs(Badge, { variant: "secondary", className: "gap-1", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "Automatisch"] })), loc.carrier_assignment_source === "manual" && (_jsx(Badge, { variant: "outline", children: "Handmatig" })), !loc.carrier_id && (_jsx(Badge, { variant: "outline", className: "text-amber-600 border-amber-300", children: "Geen match" }))] })), loc.is_primary && _jsx(Badge, { children: "Vaste locatie" })] })] }) }, loc.id))) })), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: form.id ? "Locatie bewerken" : "Nieuwe locatie" }), _jsx(DialogDescription, { children: "De vervoerder wordt automatisch gekozen op basis van plaats, provincie of land." })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { children: "Locatienaam (optioneel)" }), _jsx(Input, { value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }) })] }), _jsxs("div", { className: "md:col-span-2 grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "col-span-2", children: [_jsx(Label, { children: "Straat" }), _jsx(Input, { value: form.street, onChange: (e) => setForm({ ...form, street: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Huisnummer" }), _jsx(Input, { value: form.house_number, onChange: (e) => setForm({ ...form, house_number: e.target.value }) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Postcode" }), _jsx(Input, { value: form.postal_code, onChange: (e) => setForm({ ...form, postal_code: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Plaats" }), _jsx(Input, { value: form.city, onChange: (e) => setForm({ ...form, city: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Land" }), _jsxs(Select, { value: findCountry(form.country)?.name ?? "", onValueChange: (v) => setForm({ ...form, country: v, province: "" }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een land" }) }), _jsx(SelectContent, { children: EU_COUNTRIES.map((c) => (_jsx(SelectItem, { value: c.name, children: c.name }, c.code))) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Provincie" }), (() => {
80
+ const provinces = findCountry(form.country)?.provinces ?? [];
81
+ return provinces.length > 0 ? (_jsxs(Select, { value: form.province || "", onValueChange: (v) => setForm({ ...form, province: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een provincie" }) }), _jsx(SelectContent, { children: provinces.map((p) => (_jsx(SelectItem, { value: p, children: p }, p))) })] })) : (_jsx(Input, { value: form.province, onChange: (e) => setForm({ ...form, province: e.target.value }), placeholder: "Kies eerst een land", disabled: !form.country }));
82
+ })()] }), _jsxs("div", { className: "md:col-span-2 space-y-2", children: [_jsx(Label, { children: "Vervoerder" }), _jsxs(Select, { value: form.self_pickup ? "__self__" : (form.carrier_id ?? "__none__"), onValueChange: (v) => {
83
+ if (v === "__self__") {
84
+ setForm({ ...form, self_pickup: true, carrier_id: null, carrier_assignment_source: "manual" });
85
+ }
86
+ else if (v === "__none__") {
87
+ setForm({ ...form, self_pickup: false, carrier_id: null, carrier_assignment_source: "manual" });
88
+ }
89
+ else {
90
+ setForm({ ...form, self_pickup: false, carrier_id: v, carrier_assignment_source: "manual" });
91
+ }
92
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een vervoerder" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__self__", children: "Eigen vervoer (klant haalt zelf op)" }), _jsx(SelectItem, { value: "__none__", children: "\u2014 Geen vervoerder \u2014" }), carriers.map((c) => (_jsx(SelectItem, { value: c.id, children: c.name }, c.id)))] })] }), form.self_pickup ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "De klant komt zelf de karren ophalen \u2014 er wordt geen vervoerder gekoppeld." })) : form.carrier_assignment_source === "manual" ? (_jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsx("span", { children: "Handmatig overschreven." }), _jsx(Button, { variant: "link", size: "sm", className: "h-auto p-0", onClick: () => setForm({ ...form, carrier_assignment_source: "auto" }), children: "Terug naar automatisch" })] })) : autoSuggested?.carrier_id ? (_jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "Automatisch geselecteerd op basis van ", autoSuggested.matched_on === "city"
93
+ ? "plaats" : autoSuggested.matched_on === "province" ? "provincie" : "land", "."] })) : (_jsx("div", { className: "text-xs text-amber-600", children: "Geen vervoerder gevonden voor dit bedieningsgebied \u2014 selecteer er handmatig \u00E9\u00E9n." }))] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setOpen(false), children: "Annuleren" }), _jsx(Button, { onClick: () => saveMut.mutate(), disabled: saveMut.isPending, children: form.id ? "Opslaan" : "Toevoegen" })] })] }) })] }));
94
+ }
@@ -0,0 +1,4 @@
1
+ export declare function GroupedKlantDetail({ groupKey }: {
2
+ groupKey: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=GroupedKlantDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAoBA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA4MpE"}
@@ -0,0 +1,152 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { useState } from "react";
6
+ import { Card, CardContent, Badge, Button, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
7
+ import { ArrowLeft, Mail, Phone } from "lucide-react";
8
+ import { getGroupedCustomerDetail, } from "../../lib/external-customers.functions";
9
+ import { ProfileCard, KpiStrip, BadgeList, EmptyState, adaptGroupedToProfile, } from "../../components/klanten/KlantDetailLayout";
10
+ export function GroupedKlantDetail({ groupKey }) {
11
+ const navigate = useNavigate();
12
+ const get = useServerFn(getGroupedCustomerDetail);
13
+ const [tab, setTab] = useState("overview");
14
+ const { data, isLoading, error } = useQuery({
15
+ queryKey: ["grouped-customer", groupKey],
16
+ queryFn: () => get({ data: { groupKey } }),
17
+ });
18
+ if (isLoading)
19
+ return _jsx("main", { className: "p-6", children: "Laden..." });
20
+ if (error || !data) {
21
+ return (_jsxs("main", { className: "p-6 space-y-3", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug naar klanten"] }), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Klant niet gevonden." })] }));
22
+ }
23
+ const g = data.group;
24
+ const providerMap = new Map(data.providers.map((p) => [p.provider, p]));
25
+ const sources = g.sources ?? [];
26
+ const profile = adaptGroupedToProfile(g);
27
+ const contacts = collectContacts(sources, providerMap);
28
+ const locations = collectLocations(sources, providerMap);
29
+ const segments = collectArray(sources, ["segments"]);
30
+ const tradeForms = collectArray(sources, ["trade_forms", "tradeForms"]);
31
+ const markets = collectArray(sources, ["markets"]);
32
+ const productGroups = collectArray(sources, ["product_groups", "productGroups"]);
33
+ const paymentMethods = collectArray(sources, ["payment_methods", "paymentMethods"]);
34
+ const activity = sources
35
+ .filter((s) => s.fetched_at)
36
+ .map((s) => ({
37
+ id: `${s.provider}-${s.external_id}`,
38
+ action: "sync",
39
+ created_at: s.fetched_at,
40
+ provider: providerMap.get(s.provider)?.label ?? s.provider,
41
+ }))
42
+ .sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
43
+ return (_jsxs("main", { className: "p-6 max-w-7xl mx-auto space-y-4", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug naar klanten"] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsx(KpiStrip, { counts: {
44
+ contacts: contacts.length,
45
+ locations: locations.length,
46
+ assortment: 0,
47
+ orders: 0,
48
+ lastSync: g.fetched_at,
49
+ }, 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" }), _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: locations.length === 0 ? (_jsx(EmptyState, { message: "Geen afleverlocaties beschikbaar via de koppeling." })) : (_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 || "—" }), _jsx(TableCell, { className: "text-sm", children: [l.postal_code, l.country].filter(Boolean).join(" · ") }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })) }), _jsx(TabsContent, { value: "assortment", children: _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." }) }), _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 }) }))] })] })] })] }));
50
+ }
51
+ function collectContacts(sources, providerMap) {
52
+ const out = [];
53
+ for (const s of sources) {
54
+ const label = providerMap.get(s.provider)?.label ?? s.provider;
55
+ const raw = s.raw ?? {};
56
+ const candidates = [];
57
+ if (raw.primaryContact)
58
+ candidates.push(raw.primaryContact);
59
+ if (Array.isArray(raw.contactPersons))
60
+ candidates.push(...raw.contactPersons);
61
+ if (Array.isArray(raw.contacts))
62
+ candidates.push(...raw.contacts);
63
+ if (candidates.length === 0 && s.contact_person) {
64
+ candidates.push({ name: s.contact_person, email: s.email, phone: s.phone });
65
+ }
66
+ for (const c of candidates) {
67
+ const full_name = c.fullName ?? c.name ?? [c.firstName, c.lastName].filter(Boolean).join(" ");
68
+ if (!full_name)
69
+ continue;
70
+ out.push({
71
+ full_name,
72
+ job_title: c.jobTitle ?? c.role ?? null,
73
+ email: c.email ?? c.emailAddress ?? null,
74
+ phone: c.phone ?? c.phoneNumber ?? null,
75
+ source_label: label,
76
+ });
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+ function collectLocations(sources, providerMap) {
82
+ const out = [];
83
+ for (const s of sources) {
84
+ const label = providerMap.get(s.provider)?.label ?? s.provider;
85
+ const raw = s.raw ?? {};
86
+ const rawLocs = Array.isArray(raw.deliveryLocations)
87
+ ? raw.deliveryLocations
88
+ : Array.isArray(raw.locations) ? raw.locations : [];
89
+ if (rawLocs.length > 0) {
90
+ for (const l of rawLocs) {
91
+ out.push({
92
+ name: l.name ?? null,
93
+ address_line: [l.street, l.houseNumber].filter(Boolean).join(" ") || l.addressLine || null,
94
+ city: l.city ?? null,
95
+ postal_code: l.postalCode ?? l.zipCode ?? null,
96
+ country: l.country ?? l.countryCode ?? null,
97
+ gln: l.gln ?? null,
98
+ source_label: label,
99
+ });
100
+ }
101
+ }
102
+ else if (s.street || s.city || s.postal_code) {
103
+ out.push({
104
+ name: null,
105
+ address_line: [s.street, s.house_number].filter(Boolean).join(" ") || null,
106
+ city: s.city,
107
+ postal_code: s.postal_code,
108
+ country: s.country,
109
+ gln: s.gln,
110
+ source_label: label,
111
+ });
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+ function collectArray(sources, keys) {
117
+ const set = new Set();
118
+ for (const s of sources) {
119
+ for (const key of keys) {
120
+ const v = s.raw?.[key];
121
+ if (Array.isArray(v))
122
+ for (const x of v)
123
+ if (x)
124
+ set.add(String(x));
125
+ }
126
+ }
127
+ return Array.from(set);
128
+ }
129
+ function CompareCard({ sources, providerMap, }) {
130
+ const fields = [
131
+ { key: "company_name", label: "Bedrijfsnaam" },
132
+ { key: "commercial_name", label: "Handelsnaam" },
133
+ { key: "contact_person", label: "Contactpersoon" },
134
+ { key: "email", label: "E-mail" },
135
+ { key: "phone", label: "Telefoon" },
136
+ { key: "street", label: "Straat" },
137
+ { key: "postal_code", label: "Postcode" },
138
+ { key: "city", label: "Plaats" },
139
+ { key: "country", label: "Land" },
140
+ { key: "website", label: "Website" },
141
+ { key: "vat_number", label: "BTW" },
142
+ { key: "gln", label: "GLN" },
143
+ ];
144
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 overflow-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b", children: [_jsx("th", { className: "text-left py-2 pr-3 text-xs font-medium uppercase text-muted-foreground", children: "Veld" }), sources.map((s) => (_jsx("th", { className: "text-left py-2 px-3 text-xs font-medium", children: providerMap.get(s.provider)?.label ?? s.provider }, s.provider)))] }) }), _jsx("tbody", { children: fields.map((f) => {
145
+ const values = sources.map((s) => s[f.key] ?? null);
146
+ const firstNonEmpty = values.find((v) => v != null && v !== "");
147
+ return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "py-2 pr-3 text-muted-foreground align-top", children: f.label }), values.map((v, i) => {
148
+ const differs = v != null && v !== "" && firstNonEmpty != null && v !== firstNonEmpty;
149
+ return (_jsx("td", { className: `py-2 px-3 align-top ${differs ? "bg-muted/40 rounded" : ""}`, children: v && v !== "" ? v : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }, i));
150
+ })] }, String(f.key)));
151
+ }) })] }) }) }));
152
+ }
@@ -0,0 +1,4 @@
1
+ export declare function KlantDetail({ id }: {
2
+ id: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=KlantDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAkPjD"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { useState } from "react";
6
+ import { Card, CardContent, CardHeader, CardTitle, Badge, Button, toast, Tabs, TabsContent, TabsList, TabsTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@flowselections/core";
7
+ import { ArrowLeft, Trash2, Mail, Phone, Power, } from "lucide-react";
8
+ import { getCustomer, deleteCustomer, updateCustomer } from "../../lib/customers.functions";
9
+ import { ProfileCard, KpiStrip, BadgeList, EmptyState, ActivityItem, adaptLocalToProfile, } from "../../components/klanten/KlantDetailLayout";
10
+ import { DeliveryLocationsTab } from "../../components/klanten/DeliveryLocationsTab";
11
+ import { CustomerAnalyticsTab } from "../../components/klanten/CustomerAnalyticsTab";
12
+ export function KlantDetail({ id }) {
13
+ const navigate = useNavigate();
14
+ const qc = useQueryClient();
15
+ const get = useServerFn(getCustomer);
16
+ const del = useServerFn(deleteCustomer);
17
+ const upd = useServerFn(updateCustomer);
18
+ const [tab, setTab] = useState("overview");
19
+ const { data, isLoading } = useQuery({
20
+ queryKey: ["customer", id],
21
+ queryFn: () => get({ data: { id } }),
22
+ });
23
+ const delMut = useMutation({
24
+ mutationFn: () => del({ data: { id } }),
25
+ onSuccess: () => { toast.success("Verwijderd"); navigate({ to: "/klanten" }); },
26
+ });
27
+ const toggleActive = useMutation({
28
+ mutationFn: (active) => upd({ data: { id, is_active: active } }),
29
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["customer", id] }),
30
+ });
31
+ if (isLoading)
32
+ return _jsx("main", { className: "p-6", children: "Laden..." });
33
+ if (!data)
34
+ return _jsx("main", { className: "p-6", children: "Klant niet gevonden" });
35
+ const c = data.customer;
36
+ return (_jsxs("main", { className: "p-6 max-w-7xl mx-auto space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug naar klanten"] }), _jsxs("div", { className: "flex gap-2", children: [_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) }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsx(KpiStrip, { counts: {
37
+ contacts: data.contacts.length,
38
+ locations: data.delivery_locations.length,
39
+ assortment: data.assortment.length,
40
+ orders: data.orders.length,
41
+ lastSync: c.last_synced_at,
42
+ }, 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, ")"] }), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _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.notes && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Notities" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.notes })] })), !c.bio && !c.notes && !(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))) })] }) }) })) }), _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))) }) }) })] })] })] })] }));
43
+ }
@@ -0,0 +1,75 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ExternalCustomer, GroupedCustomer } from "../../lib/external-customers.functions";
3
+ export type CustomerProfile = {
4
+ logo_url: string | null;
5
+ company_name: string;
6
+ commercial_name: string | null;
7
+ is_active: boolean;
8
+ class_badge: string | null;
9
+ email: string | null;
10
+ phone: string | null;
11
+ website: string | null;
12
+ street: string | null;
13
+ house_number: string | null;
14
+ postal_code: string | null;
15
+ city: string | null;
16
+ country: string | null;
17
+ vat_number: string | null;
18
+ gln: string | null;
19
+ rfh_id: string | null;
20
+ organization_type: string | null;
21
+ primary_contact_name: string | null;
22
+ email_from_contact: boolean;
23
+ phone_from_contact: boolean;
24
+ has_contacts: boolean;
25
+ };
26
+ export declare function formatCountry(value: string | null | undefined): string | null;
27
+ export declare function adaptLocalToProfile(c: any, contacts?: any[]): CustomerProfile;
28
+ export declare function adaptGroupedToProfile(g: GroupedCustomer): CustomerProfile;
29
+ export declare function adaptExternalToProfile(c: ExternalCustomer): CustomerProfile;
30
+ export declare function ProfileCard({ profile: p }: {
31
+ profile: CustomerProfile;
32
+ }): import("react").JSX.Element;
33
+ export declare function SectionLabel({ children }: {
34
+ children: ReactNode;
35
+ }): import("react").JSX.Element;
36
+ export declare function QuickAction({ icon, label, href, external }: {
37
+ icon: ReactNode;
38
+ label: string;
39
+ href?: string;
40
+ external?: boolean;
41
+ }): import("react").JSX.Element;
42
+ export declare function InfoRow({ icon, label, value, href, external, mono, hint, }: {
43
+ icon: ReactNode;
44
+ label?: string;
45
+ value: string | null | undefined;
46
+ href?: string;
47
+ external?: boolean;
48
+ mono?: boolean;
49
+ hint?: string;
50
+ }): import("react").JSX.Element;
51
+ export declare function KpiStrip({ counts, onSelect, }: {
52
+ counts: {
53
+ contacts: number;
54
+ locations: number;
55
+ assortment: number;
56
+ orders: number;
57
+ lastSync: string | null | undefined;
58
+ };
59
+ onSelect: (tab: string) => void;
60
+ }): import("react").JSX.Element;
61
+ export declare function ActivityItem({ entry }: {
62
+ entry: {
63
+ id?: string;
64
+ action: string;
65
+ created_at: string;
66
+ };
67
+ }): import("react").JSX.Element;
68
+ export declare function BadgeList({ label, items }: {
69
+ label: string;
70
+ items: string[] | null | undefined;
71
+ }): import("react").JSX.Element;
72
+ export declare function EmptyState({ message }: {
73
+ message: string;
74
+ }): import("react").JSX.Element;
75
+ //# sourceMappingURL=KlantDetailLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KlantDetailLayout.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetailLayout.tsx"],"names":[],"mappings":"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"}