@flowselections/floriday-klanten-module 1.0.7 → 1.0.9

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 (37) 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/KlantDetailLayout.d.ts +75 -0
  8. package/dist-lib/components/klanten/KlantDetailLayout.d.ts.map +1 -0
  9. package/dist-lib/components/klanten/KlantDetailLayout.js +172 -0
  10. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  11. package/dist-lib/components/klanten/KlantenOverzicht.js +12 -32
  12. package/dist-lib/index.js +1 -1
  13. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3903 -361
  14. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  15. package/dist-lib/integrations/supabase/client.d.ts +3903 -361
  16. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  17. package/dist-lib/integrations/supabase/client.server.d.ts +3903 -361
  18. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  19. package/dist-lib/integrations/supabase/types.d.ts +3994 -340
  20. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/types.js +10 -1
  22. package/dist-lib/lib/accounting.functions.d.ts +19056 -8430
  23. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  24. package/dist-lib/lib/customer-analytics.functions.d.ts +9287 -0
  25. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -0
  26. package/dist-lib/lib/customer-analytics.functions.js +165 -0
  27. package/dist-lib/lib/customers.functions.d.ts +46628 -21828
  28. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/delivery-locations.functions.d.ts +37018 -0
  30. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -0
  31. package/dist-lib/lib/delivery-locations.functions.js +150 -0
  32. package/dist-lib/lib/eu-regions.d.ts +14 -0
  33. package/dist-lib/lib/eu-regions.d.ts.map +1 -0
  34. package/dist-lib/lib/eu-regions.js +68 -0
  35. package/dist-lib/lib/external-customers.functions.d.ts +26750 -12582
  36. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  37. 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,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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"}
@@ -0,0 +1,172 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Card, CardContent, Badge, Separator, } from "@flowselections/core";
4
+ import { Mail, Phone, Globe, MapPin, Hash, Building2, ChevronDown, ChevronRight, Users, Truck, Package, ShoppingCart, Clock, } from "lucide-react";
5
+ import { CustomerAvatar } from "../CustomerAvatar";
6
+ function firstNonEmpty(values) {
7
+ for (const v of values) {
8
+ if (v != null && (typeof v !== "string" || v.trim() !== ""))
9
+ return v;
10
+ }
11
+ return null;
12
+ }
13
+ const COUNTRY_NAMES = {
14
+ NL: "Nederland", BE: "België", DE: "Duitsland", FR: "Frankrijk",
15
+ ES: "Spanje", IT: "Italië", GB: "Verenigd Koninkrijk", UK: "Verenigd Koninkrijk",
16
+ PL: "Polen", DK: "Denemarken", SE: "Zweden", NO: "Noorwegen",
17
+ AT: "Oostenrijk", CH: "Zwitserland", PT: "Portugal", IE: "Ierland",
18
+ LU: "Luxemburg", CZ: "Tsjechië", FI: "Finland", US: "Verenigde Staten",
19
+ };
20
+ export function formatCountry(value) {
21
+ if (!value)
22
+ return null;
23
+ const trimmed = value.trim();
24
+ if (trimmed.length === 2)
25
+ return COUNTRY_NAMES[trimmed.toUpperCase()] ?? trimmed;
26
+ return trimmed;
27
+ }
28
+ export function adaptLocalToProfile(c, contacts = []) {
29
+ const primary = contacts.find((x) => x.is_active !== false) ?? contacts[0] ?? null;
30
+ const email = c.email ?? primary?.email ?? null;
31
+ const phone = c.phone ?? primary?.phone ?? null;
32
+ const rfh = c.custom_fields?.rfh_relation_id ??
33
+ c.floriday_raw?.rfhRelationId ??
34
+ c.floriday_raw?.rfhCompanyAccountId ??
35
+ null;
36
+ return {
37
+ logo_url: c.logo_url ?? null,
38
+ company_name: c.company_name,
39
+ commercial_name: c.commercial_name ?? null,
40
+ is_active: !!c.is_active,
41
+ class_badge: c.customer_class ?? null,
42
+ email,
43
+ phone,
44
+ website: c.website ?? null,
45
+ street: c.street ?? null,
46
+ house_number: c.house_number ?? null,
47
+ postal_code: c.postal_code ?? null,
48
+ city: c.city ?? null,
49
+ country: formatCountry(c.country ?? null),
50
+ vat_number: c.vat_number ?? null,
51
+ gln: c.gln ?? null,
52
+ rfh_id: rfh ? String(rfh) : null,
53
+ organization_type: c.organization_type ?? null,
54
+ primary_contact_name: primary?.full_name ?? null,
55
+ email_from_contact: !c.email && !!primary?.email,
56
+ phone_from_contact: !c.phone && !!primary?.phone,
57
+ has_contacts: contacts.length > 0,
58
+ };
59
+ }
60
+ export function adaptGroupedToProfile(g) {
61
+ const sources = g.sources ?? [];
62
+ const pick = (k) => firstNonEmpty(sources.map((s) => s[k]));
63
+ const rfh = firstNonEmpty(sources.map((s) => s.raw?.custom_fields?.rfh_relation_id ??
64
+ s.raw?.custom_fields?.rfh_company_account_id ??
65
+ s.raw?.rfhRelationId ??
66
+ s.raw?.rfhCompanyAccountId)) ?? null;
67
+ const orgType = firstNonEmpty(sources.map((s) => s.raw?.organization_type ?? s.raw?.organizationType));
68
+ return {
69
+ logo_url: g.logo_url ?? pick("logo_url") ?? null,
70
+ company_name: g.display_name,
71
+ commercial_name: g.commercial_name ?? pick("commercial_name") ?? null,
72
+ is_active: !!g.is_active,
73
+ class_badge: null,
74
+ email: g.email ?? pick("email"),
75
+ phone: g.phone ?? pick("phone"),
76
+ website: g.website ?? pick("website"),
77
+ street: pick("street"),
78
+ house_number: pick("house_number"),
79
+ postal_code: pick("postal_code"),
80
+ city: g.city ?? pick("city"),
81
+ country: formatCountry(g.country ?? pick("country")),
82
+ vat_number: g.vat_number ?? pick("vat_number"),
83
+ gln: g.gln ?? pick("gln"),
84
+ rfh_id: rfh ? String(rfh) : null,
85
+ organization_type: orgType ? String(orgType) : null,
86
+ primary_contact_name: g.contact_person ?? pick("contact_person"),
87
+ email_from_contact: false,
88
+ phone_from_contact: false,
89
+ has_contacts: !!(g.contact_person || pick("contact_person")),
90
+ };
91
+ }
92
+ export function adaptExternalToProfile(c) {
93
+ const rfh = c.raw?.custom_fields?.rfh_relation_id ??
94
+ c.raw?.custom_fields?.rfh_company_account_id ??
95
+ c.raw?.rfhRelationId ??
96
+ c.raw?.rfhCompanyAccountId ??
97
+ null;
98
+ const orgType = c.raw?.organization_type ?? c.raw?.organizationType ?? null;
99
+ return {
100
+ logo_url: c.logo_url,
101
+ company_name: c.company_name,
102
+ commercial_name: c.commercial_name,
103
+ is_active: !!c.is_active,
104
+ class_badge: null,
105
+ email: c.email,
106
+ phone: c.phone,
107
+ website: c.website,
108
+ street: c.street,
109
+ house_number: c.house_number,
110
+ postal_code: c.postal_code,
111
+ city: c.city,
112
+ country: formatCountry(c.country),
113
+ vat_number: c.vat_number,
114
+ gln: c.gln,
115
+ rfh_id: rfh ? String(rfh) : null,
116
+ organization_type: orgType ? String(orgType) : null,
117
+ primary_contact_name: c.contact_person,
118
+ email_from_contact: false,
119
+ phone_from_contact: false,
120
+ has_contacts: !!c.contact_person,
121
+ };
122
+ }
123
+ export function ProfileCard({ profile: p }) {
124
+ const address = [p.street, p.house_number].filter(Boolean).join(" ");
125
+ const cityLine = [p.postal_code, p.city].filter(Boolean).join(" ");
126
+ const fullAddress = [address, cityLine, p.country].filter(Boolean).join(", ");
127
+ const [showTech, setShowTech] = useState(false);
128
+ const hasTech = !!(p.gln || p.rfh_id || p.organization_type);
129
+ return (_jsx(Card, { className: "lg:sticky lg:top-6 self-start", children: _jsxs(CardContent, { className: "pt-6 space-y-5", children: [_jsxs("div", { className: "flex flex-col items-center text-center", children: [_jsx(CustomerAvatar, { logoUrl: p.logo_url, name: p.company_name, size: "lg", className: "h-24 w-24 text-2xl" }), _jsx("h1", { className: "mt-3 text-xl font-bold leading-tight", children: p.company_name }), p.commercial_name && p.commercial_name !== p.company_name && (_jsx("p", { className: "text-sm text-muted-foreground", children: p.commercial_name })), _jsxs("div", { className: "mt-3 flex flex-wrap items-center justify-center gap-1.5", children: [p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "destructive", children: "Inactief" }), p.class_badge && _jsx(Badge, { variant: "secondary", children: p.class_badge })] })] }), (p.email || p.phone || p.website) && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { children: [_jsx(SectionLabel, { children: "Snelle acties" }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsx(QuickAction, { icon: _jsx(Mail, { className: "h-4 w-4" }), label: "E-mail", href: p.email ? `mailto:${p.email}` : undefined }), _jsx(QuickAction, { icon: _jsx(Phone, { className: "h-4 w-4" }), label: "Bellen", href: p.phone ? `tel:${p.phone}` : undefined }), _jsx(QuickAction, { icon: _jsx(Globe, { className: "h-4 w-4" }), label: "Website", href: p.website ?? undefined, external: true })] })] })] })), _jsx(Separator, {}), _jsxs("div", { className: "space-y-3", children: [_jsx(SectionLabel, { children: "Contact" }), _jsx(InfoRow, { icon: _jsx(MapPin, { className: "h-4 w-4" }), value: fullAddress || null }), _jsx(InfoRow, { icon: _jsx(Mail, { className: "h-4 w-4" }), value: p.email, href: p.email ? `mailto:${p.email}` : undefined, hint: p.email_from_contact ? `via ${p.primary_contact_name ?? "contactpersoon"}` : undefined }), _jsx(InfoRow, { icon: _jsx(Phone, { className: "h-4 w-4" }), value: p.phone, href: p.phone ? `tel:${p.phone}` : undefined, hint: p.phone_from_contact ? `via ${p.primary_contact_name ?? "contactpersoon"}` : undefined }), _jsx(InfoRow, { icon: _jsx(Globe, { className: "h-4 w-4" }), value: p.website, href: p.website ?? undefined, external: true }), !p.email && !p.phone && !p.has_contacts && (_jsxs("p", { className: "text-xs text-muted-foreground leading-relaxed", children: ["Voeg e-mail of telefoon toe via ", _jsx("strong", { children: "Bewerken" }), "."] }))] }), p.vat_number && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { className: "space-y-3", children: [_jsx(SectionLabel, { children: "Identificatie" }), _jsx(InfoRow, { icon: _jsx(Hash, { className: "h-4 w-4" }), label: "BTW", value: p.vat_number, mono: true })] })] })), hasTech && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("button", { type: "button", onClick: () => setShowTech((v) => !v), className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [showTech ? _jsx(ChevronDown, { className: "h-3 w-3" }) : _jsx(ChevronRight, { className: "h-3 w-3" }), "Technische gegevens"] }), showTech && (_jsxs("div", { className: "space-y-3", children: [p.rfh_id && _jsx(InfoRow, { icon: _jsx(Hash, { className: "h-4 w-4" }), label: "RFH-relatienr.", value: p.rfh_id, mono: true }), _jsx(InfoRow, { icon: _jsx(Hash, { className: "h-4 w-4" }), label: "GLN", value: p.gln, mono: true }), _jsx(InfoRow, { icon: _jsx(Building2, { className: "h-4 w-4" }), label: "Type", value: p.organization_type })] }))] }))] }) }));
130
+ }
131
+ export function SectionLabel({ children }) {
132
+ return _jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2", children: children });
133
+ }
134
+ export function QuickAction({ icon, label, href, external }) {
135
+ const disabled = !href;
136
+ const cls = "flex flex-col items-center justify-center gap-1 rounded-md border border-border bg-background p-2.5 text-xs transition-colors hover:bg-muted disabled:opacity-40 disabled:hover:bg-background";
137
+ if (disabled) {
138
+ return _jsxs("button", { type: "button", disabled: true, className: cls, children: [icon, _jsx("span", { children: label })] });
139
+ }
140
+ return (_jsxs("a", { href: href, target: external ? "_blank" : undefined, rel: external ? "noopener noreferrer" : undefined, className: cls, children: [icon, _jsx("span", { children: label })] }));
141
+ }
142
+ export function InfoRow({ icon, label, value, href, external, mono, hint, }) {
143
+ const content = value ?? "—";
144
+ return (_jsxs("div", { className: "flex items-start gap-3 text-sm", children: [_jsx("span", { className: "text-muted-foreground mt-0.5 shrink-0", children: icon }), _jsxs("div", { className: "min-w-0 flex-1", children: [label && _jsx("div", { className: "text-xs text-muted-foreground", children: label }), href && value ? (_jsx("a", { href: href, target: external ? "_blank" : undefined, rel: external ? "noopener noreferrer" : undefined, className: `hover:underline break-words ${mono ? "font-mono text-xs" : ""}`, children: content })) : (_jsx("div", { className: `break-words ${mono ? "font-mono text-xs" : ""} ${!value ? "text-muted-foreground" : ""}`, children: content })), hint && value && (_jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: hint }))] })] }));
145
+ }
146
+ export function KpiStrip({ counts, onSelect, }) {
147
+ const items = [
148
+ { key: "contacts", icon: _jsx(Users, { className: "h-4 w-4" }), label: "Contacten", value: counts.contacts },
149
+ { key: "locations", icon: _jsx(Truck, { className: "h-4 w-4" }), label: "Locaties", value: counts.locations },
150
+ { key: "assortment", icon: _jsx(Package, { className: "h-4 w-4" }), label: "Assortiment", value: counts.assortment },
151
+ { key: "orders", icon: _jsx(ShoppingCart, { className: "h-4 w-4" }), label: "Bestellingen", value: counts.orders },
152
+ ];
153
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-5 divide-y md:divide-y-0 md:divide-x divide-border", children: [items.map((it) => (_jsxs("button", { type: "button", onClick: () => onSelect(it.key), className: "flex flex-col items-start gap-1 p-4 text-left transition-colors hover:bg-muted/50", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [it.icon, " ", it.label] }), _jsx("span", { className: "text-2xl font-bold", children: it.value })] }, it.key))), _jsxs("div", { className: "flex flex-col items-start gap-1 p-4 col-span-2 md:col-span-1", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(Clock, { className: "h-4 w-4" }), " Laatste sync"] }), _jsx("span", { className: "text-sm font-medium", children: counts.lastSync ? new Date(counts.lastSync).toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" }) : "—" })] })] }) }) }));
154
+ }
155
+ export function ActivityItem({ entry }) {
156
+ const actionLabels = {
157
+ create: "Aangemaakt",
158
+ insert: "Aangemaakt",
159
+ update: "Bijgewerkt",
160
+ delete: "Verwijderd",
161
+ sync: "Bijgewerkt vanuit extern systeem",
162
+ };
163
+ const label = actionLabels[entry.action] ?? "Wijziging";
164
+ return (_jsx("div", { className: "border-l-2 border-primary pl-3 py-1", children: _jsxs("div", { className: "flex justify-between items-center text-sm", children: [_jsx("strong", { children: label }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(entry.created_at).toLocaleString("nl-NL") })] }) }));
165
+ }
166
+ export function BadgeList({ label, items }) {
167
+ const list = items ?? [];
168
+ return (_jsxs("div", { children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground mb-1", children: label }), list.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground", children: "\u2014" })) : (_jsx("div", { className: "flex flex-wrap gap-1", children: list.map((s) => (_jsx(Badge, { variant: "secondary", children: s }, s))) }))] }));
169
+ }
170
+ export function EmptyState({ message }) {
171
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 text-center text-sm text-muted-foreground", children: message }) }));
172
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,gBAAgB,gCAuW/B"}
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,gBAAgB,gCAoT/B"}