@flowselections/floriday-klanten-module 1.0.6 → 1.0.8

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 (25) hide show
  1. package/dist-lib/components/klanten/ExternalCacheBanner.d.ts +6 -0
  2. package/dist-lib/components/klanten/ExternalCacheBanner.d.ts.map +1 -0
  3. package/dist-lib/components/klanten/ExternalCacheBanner.js +22 -0
  4. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  5. package/dist-lib/components/klanten/KlantenOverzicht.js +111 -79
  6. package/dist-lib/integrations/supabase/auth-middleware.d.ts +5256 -1067
  7. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  8. package/dist-lib/integrations/supabase/client.d.ts +5256 -1067
  9. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  10. package/dist-lib/integrations/supabase/client.server.d.ts +5256 -1067
  11. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  12. package/dist-lib/integrations/supabase/types.d.ts +5413 -1112
  13. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/types.js +18 -1
  15. package/dist-lib/lib/accounting/floriday/sync.server.d.ts.map +1 -1
  16. package/dist-lib/lib/accounting/floriday/sync.server.js +5 -3
  17. package/dist-lib/lib/accounting.functions.d.ts +16353 -3786
  18. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  19. package/dist-lib/lib/customers.functions.d.ts +35873 -13898
  20. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  21. package/dist-lib/lib/customers.functions.js +8 -31
  22. package/dist-lib/lib/external-customers.functions.d.ts +26610 -0
  23. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -0
  24. package/dist-lib/lib/external-customers.functions.js +131 -0
  25. package/package.json +2 -2
@@ -0,0 +1,6 @@
1
+ import type { ExternalProvider } from "../../lib/external-customers.functions";
2
+ export declare function ExternalCacheBanner({ lastFetchedAt, providers, }: {
3
+ lastFetchedAt: string | null;
4
+ providers: ExternalProvider[];
5
+ }): import("react").JSX.Element;
6
+ //# sourceMappingURL=ExternalCacheBanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExternalCacheBanner.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ExternalCacheBanner.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAa/E,wBAAgB,mBAAmB,CAAC,EAClC,aAAa,EACb,SAAS,GACV,EAAE;IACD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,gBAAgB,EAAE,CAAC;CAC/B,+BAqBA"}
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Cloud, Clock } from "lucide-react";
3
+ function relativeTime(iso) {
4
+ const diff = Date.now() - new Date(iso).getTime();
5
+ const min = Math.floor(diff / 60000);
6
+ if (min < 1)
7
+ return "zojuist";
8
+ if (min < 60)
9
+ return `${min} min geleden`;
10
+ const hr = Math.floor(min / 60);
11
+ if (hr < 24)
12
+ return `${hr} uur geleden`;
13
+ const day = Math.floor(hr / 24);
14
+ return `${day} dag${day === 1 ? "" : "en"} geleden`;
15
+ }
16
+ export function ExternalCacheBanner({ lastFetchedAt, providers, }) {
17
+ const labels = providers
18
+ .filter((p) => p.is_enabled)
19
+ .map((p) => p.label)
20
+ .join(", ");
21
+ return (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-border bg-muted/40 px-4 py-3 text-sm", children: [_jsx(Cloud, { className: "h-4 w-4 mt-0.5 text-muted-foreground shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium", children: "Externe klantgegevens (read-only)" }), _jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: ["Data wordt automatisch verfrist door de gekoppelde authenticatiemodules", labels ? `: ${labels}` : "", "."] })] }), _jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground shrink-0", children: [_jsx(Clock, { className: "h-3.5 w-3.5" }), lastFetchedAt ? `Laatste cache: ${relativeTime(lastFetchedAt)}` : "Nog geen cache"] })] }));
22
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAiBA,wBAAgB,gBAAgB,gCAmT/B"}
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,gBAAgB,gCAuW/B"}
@@ -1,109 +1,136 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useNavigate } from "@tanstack/react-router";
3
- import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import { useQuery } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
- import { useEffect, useMemo, useRef, useState } from "react";
6
- import { Button, Input, Card, CardContent, Badge, toast, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Skeleton, } from "@flowselections/core";
7
- import { Plus, RefreshCw, Search, Network, Users, UserCheck, UserX, Cloud, HardDrive, Clock, X, ArrowUpDown, Mail, MapPin, } from "lucide-react";
8
- import { listCustomers, syncFloridayCustomers, listCustomerClasses } from "../../lib/customers.functions";
5
+ import { useMemo, useState } from "react";
6
+ import { Button, Input, Card, CardContent, Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Skeleton, } from "@flowselections/core";
7
+ import { Plus, Search, Network, Users, UserCheck, UserX, Cloud, HardDrive, X, ArrowUpDown, Mail, MapPin, } from "lucide-react";
8
+ import { listCustomers, listCustomerClasses } from "../../lib/customers.functions";
9
+ import { listExternalCustomers } from "../../lib/external-customers.functions";
9
10
  import { CustomerAvatar } from "../CustomerAvatar";
11
+ import { ExternalCacheBanner } from "./ExternalCacheBanner";
10
12
  export function KlantenOverzicht() {
11
13
  const navigate = useNavigate();
12
- const qc = useQueryClient();
13
14
  const [search, setSearch] = useState("");
14
15
  const [customerClass, setCustomerClass] = useState("__all__");
15
16
  const [country, setCountry] = useState("__all__");
16
17
  const [view, setView] = useState("all");
17
18
  const [sort, setSort] = useState({ col: "company_name", dir: "asc" });
18
19
  const list = useServerFn(listCustomers);
19
- const sync = useServerFn(syncFloridayCustomers);
20
+ const listExternal = useServerFn(listExternalCustomers);
20
21
  const classesFn = useServerFn(listCustomerClasses);
21
- const activeFilter = view === "active" ? "active" : view === "inactive" ? "inactive" : "all";
22
- const source = view === "floriday" ? "floriday" : view === "local" ? "local" : "all";
23
22
  const { data: classData } = useQuery({
24
23
  queryKey: ["customer-classes"],
25
24
  queryFn: () => classesFn({ data: undefined }),
26
25
  });
27
- const { data: totalsData } = useQuery({
28
- queryKey: ["customers", "totals"],
26
+ const { data: localData, isLoading: loadingLocal } = useQuery({
27
+ queryKey: ["customers-local"],
29
28
  queryFn: () => list({
30
- data: { search: "", customer_class: "", country: "", is_active: "all", source: "all" },
29
+ data: { search: "", customer_class: "", country: "", is_active: "all", source: "local" },
31
30
  }),
32
31
  });
33
- const { data, isLoading } = useQuery({
34
- queryKey: ["customers", { search, customerClass, activeFilter, source }],
35
- queryFn: () => list({
36
- data: {
37
- search,
38
- customer_class: customerClass === "__all__" ? "" : customerClass,
39
- country: "",
40
- is_active: activeFilter,
41
- source,
42
- },
43
- }),
32
+ const { data: externalData, isLoading: loadingExternal } = useQuery({
33
+ queryKey: ["customers-external"],
34
+ queryFn: () => listExternal({ data: { status: "all" } }),
44
35
  });
45
- const syncMut = useMutation({
46
- mutationFn: (_variables) => sync({ data: undefined }),
47
- onSuccess: (r, variables) => {
48
- const showToast = variables?.showToast ?? true;
49
- if (showToast) {
50
- if (r.errors.length) {
51
- const firstError = r.errors[0]?.message ?? "Onbekende Floriday-fout";
52
- toast.error(r.errors.length === 1 ? firstError : `${r.errors.length} fouten bij sync. Eerste fout: ${firstError}`);
53
- }
54
- else {
55
- toast.success(`Floriday gesynchroniseerd (${r.synced} klanten)`);
56
- }
57
- }
58
- qc.invalidateQueries({ queryKey: ["customers"] });
59
- },
60
- onError: (e, variables) => {
61
- if (variables?.showToast ?? true)
62
- toast.error(e instanceof Error ? e.message : "Sync mislukt");
63
- },
64
- });
65
- const autoSyncedRef = useRef(false);
66
- useEffect(() => {
67
- if (autoSyncedRef.current)
68
- return;
69
- autoSyncedRef.current = true;
70
- syncMut.mutate({ showToast: false });
71
- // eslint-disable-next-line react-hooks/exhaustive-deps
72
- }, []);
73
- const all = totalsData?.customers ?? [];
74
- const totals = useMemo(() => ({
75
- all: all.length,
76
- active: all.filter((c) => c.is_active).length,
77
- inactive: all.filter((c) => !c.is_active).length,
78
- floriday: all.filter((c) => c.source === "floriday").length,
79
- local: all.filter((c) => c.source === "local").length,
80
- lastSync: all.reduce((acc, c) => {
81
- if (!c.last_synced_at)
82
- return acc;
83
- if (!acc || new Date(c.last_synced_at) > new Date(acc))
84
- return c.last_synced_at;
85
- return acc;
86
- }, null),
87
- }), [all]);
36
+ const isLoading = loadingLocal || loadingExternal;
37
+ const providers = externalData?.providers ?? [];
38
+ const providerLabel = useMemo(() => {
39
+ const m = new Map();
40
+ for (const p of providers)
41
+ m.set(p.provider, p.label);
42
+ return m;
43
+ }, [providers]);
44
+ const allRows = useMemo(() => {
45
+ const local = (localData?.customers ?? []).map((c) => ({
46
+ key: `local-${c.id}`,
47
+ id: c.id,
48
+ group_key: null,
49
+ source: "local",
50
+ source_label: "Lokaal",
51
+ providers: [],
52
+ provider_labels: [],
53
+ source_count: 1,
54
+ company_name: c.company_name,
55
+ commercial_name: c.commercial_name ?? null,
56
+ contact_person: c.contact_person ?? null,
57
+ email: c.email ?? null,
58
+ city: c.city ?? null,
59
+ country: c.country ?? null,
60
+ customer_class: c.customer_class ?? null,
61
+ logo_url: c.logo_url ?? null,
62
+ is_active: !!c.is_active,
63
+ }));
64
+ const ext = (externalData?.customers ?? []).map((c) => {
65
+ const provs = c.providers ?? [];
66
+ const labels = provs.map((p) => providerLabel.get(p) ?? p);
67
+ return {
68
+ key: `grp-${c.group_key}`,
69
+ id: null,
70
+ group_key: c.group_key,
71
+ source: provs[0] ?? "external",
72
+ source_label: labels[0] ?? "Extern",
73
+ providers: provs,
74
+ provider_labels: labels,
75
+ source_count: c.source_count ?? provs.length,
76
+ company_name: c.display_name,
77
+ commercial_name: c.commercial_name,
78
+ contact_person: c.contact_person,
79
+ email: c.email,
80
+ city: c.city,
81
+ country: c.country,
82
+ customer_class: null,
83
+ logo_url: c.logo_url,
84
+ is_active: c.is_active,
85
+ };
86
+ });
87
+ return [...local, ...ext];
88
+ }, [localData, externalData, providerLabel]);
89
+ const totals = useMemo(() => {
90
+ const t = {
91
+ all: allRows.length,
92
+ active: allRows.filter((c) => c.is_active).length,
93
+ inactive: allRows.filter((c) => !c.is_active).length,
94
+ local: allRows.filter((c) => c.source === "local").length,
95
+ };
96
+ for (const p of providers) {
97
+ t[p.provider] = allRows.filter((c) => c.providers.includes(p.provider)).length;
98
+ }
99
+ return t;
100
+ }, [allRows, providers]);
88
101
  const countries = useMemo(() => {
89
102
  const set = new Set();
90
- all.forEach((c) => { if (c.country)
103
+ allRows.forEach((c) => { if (c.country)
91
104
  set.add(c.country); });
92
105
  return Array.from(set).sort();
93
- }, [all]);
94
- const baseCustomers = data?.customers ?? [];
106
+ }, [allRows]);
95
107
  const customers = useMemo(() => {
96
- let rows = baseCustomers;
108
+ let rows = allRows;
109
+ if (view === "active")
110
+ rows = rows.filter((c) => c.is_active);
111
+ else if (view === "inactive")
112
+ rows = rows.filter((c) => !c.is_active);
113
+ else if (view === "local")
114
+ rows = rows.filter((c) => c.source === "local");
115
+ else if (view !== "all")
116
+ rows = rows.filter((c) => c.providers.includes(view));
117
+ if (customerClass !== "__all__")
118
+ rows = rows.filter((c) => c.customer_class === customerClass);
97
119
  if (country !== "__all__")
98
120
  rows = rows.filter((c) => c.country === country);
99
- const sorted = [...rows].sort((a, b) => {
100
- const av = (a[sort.col] ?? "").toString().toLowerCase();
101
- const bv = (b[sort.col] ?? "").toString().toLowerCase();
121
+ if (search) {
122
+ const s = search.toLowerCase();
123
+ rows = rows.filter((c) => [c.company_name, c.commercial_name, c.contact_person, c.email, c.city]
124
+ .filter(Boolean)
125
+ .some((v) => String(v).toLowerCase().includes(s)));
126
+ }
127
+ return [...rows].sort((a, b) => {
128
+ const av = String(a[sort.col] ?? "").toLowerCase();
129
+ const bv = String(b[sort.col] ?? "").toLowerCase();
102
130
  const cmp = av.localeCompare(bv);
103
131
  return sort.dir === "asc" ? cmp : -cmp;
104
132
  });
105
- return sorted;
106
- }, [baseCustomers, country, sort]);
133
+ }, [allRows, view, customerClass, country, search, sort]);
107
134
  const activeFilterChips = [];
108
135
  if (search)
109
136
  activeFilterChips.push({ key: "s", label: `Zoek: "${search}"`, clear: () => setSearch("") });
@@ -112,11 +139,16 @@ export function KlantenOverzicht() {
112
139
  if (country !== "__all__")
113
140
  activeFilterChips.push({ key: "co", label: `Land: ${country}`, clear: () => setCountry("__all__") });
114
141
  const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
115
- return (_jsxs("main", { className: "p-6 space-y-5 max-w-[1400px] mx-auto", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je relaties en synchroniseer ze met Floriday." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/klanten/netwerk" }), children: [_jsx(Network, { className: "h-4 w-4 mr-2" }), " Floriday-netwerk"] }), _jsxs(Button, { variant: "outline", onClick: () => syncMut.mutate({ showToast: true }), disabled: syncMut.isPending, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${syncMut.isPending ? "animate-spin" : ""}` }), "Synchroniseer"] }), _jsxs(Button, { onClick: () => navigate({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-5 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-4 w-4" }), label: "Totaal", value: totals.all, active: view === "all", onClick: () => setView("all") }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-4 w-4" }), label: "Actief", value: totals.active, active: view === "active", onClick: () => setView("active") }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-4 w-4" }), label: "Inactief", value: totals.inactive, active: view === "inactive", onClick: () => setView("inactive") }), _jsx(StatCard, { icon: _jsx(Cloud, { className: "h-4 w-4" }), label: "Floriday", value: totals.floriday, active: view === "floriday", onClick: () => setView("floriday") }), _jsx(StatCard, { icon: _jsx(HardDrive, { className: "h-4 w-4" }), label: "Lokaal", value: totals.local, active: view === "local", onClick: () => setView("local") })] }), _jsx(Tabs, { value: view, onValueChange: (v) => setView(v), children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle (", totals.all, ")"] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief (", totals.active, ")"] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief (", totals.inactive, ")"] }), _jsxs(TabsTrigger, { value: "floriday", children: ["Floriday (", totals.floriday, ")"] }), _jsxs(TabsTrigger, { value: "local", children: ["Lokaal (", totals.local, ")"] })] }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsxs(Select, { value: customerClass, onValueChange: setCustomerClass, children: [_jsx(SelectTrigger, { className: "w-40", children: _jsx(SelectValue, { placeholder: "Klasse" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Alle klassen" }), (classData?.classes ?? []).map((c) => (_jsx(SelectItem, { value: c, children: c }, c)))] })] }), _jsxs(Select, { value: country, onValueChange: setCountry, children: [_jsx(SelectTrigger, { className: "w-36", children: _jsx(SelectValue, { placeholder: "Land" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Alle landen" }), countries.map((c) => (_jsx(SelectItem, { value: c, children: c }, c)))] })] }), _jsxs("div", { className: "ml-auto flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(Clock, { className: "h-3.5 w-3.5" }), "Laatste sync: ", totals.lastSync ? relativeTime(totals.lastSync) : "nog niet"] })] }), activeFilterChips.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [activeFilterChips.map((chip) => (_jsxs("button", { type: "button", onClick: chip.clear, className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/50 px-3 py-1 text-xs hover:bg-muted", children: [chip.label, " ", _jsx(X, { className: "h-3 w-3" })] }, chip.key))), _jsx("button", { type: "button", onClick: () => { setSearch(""); setCustomerClass("__all__"); setCountry("__all__"); }, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx("div", { className: "text-sm text-muted-foreground", children: isLoading ? "Laden..." : `${customers.length} ${customers.length === 1 ? "klant" : "klanten"} gevonden` }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 6 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : customers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, children: _jsx(EmptyState, { onSync: () => syncMut.mutate({ showToast: true }), onAdd: () => navigate({ to: "/klanten/nieuw" }) }) }) })) : customers.map((c) => {
142
+ return (_jsxs("main", { className: "p-6 space-y-5 max-w-[1400px] mx-auto", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je lokale relaties en bekijk Floriday-klanten uit de cache." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/klanten/netwerk" }), children: [_jsx(Network, { className: "h-4 w-4 mr-2" }), " Floriday-netwerk"] }), _jsxs(Button, { onClick: () => navigate({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsx(ExternalCacheBanner, { lastFetchedAt: externalData?.last_fetched_at ?? null, providers: providers }), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-5 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-4 w-4" }), label: "Totaal", value: totals.all ?? 0, active: view === "all", onClick: () => setView("all") }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-4 w-4" }), label: "Actief", value: totals.active ?? 0, active: view === "active", onClick: () => setView("active") }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-4 w-4" }), label: "Inactief", value: totals.inactive ?? 0, active: view === "inactive", onClick: () => setView("inactive") }), _jsx(StatCard, { icon: _jsx(HardDrive, { className: "h-4 w-4" }), label: "Lokaal", value: totals.local ?? 0, active: view === "local", onClick: () => setView("local") }), providers.filter((p) => p.is_enabled).map((p) => (_jsx(StatCard, { icon: _jsx(Cloud, { className: "h-4 w-4" }), label: p.label, value: totals[p.provider] ?? 0, active: view === p.provider, onClick: () => setView(p.provider) }, p.provider)))] }), _jsx(Tabs, { value: view, onValueChange: (v) => setView(v), children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle (", totals.all ?? 0, ")"] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief (", totals.active ?? 0, ")"] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief (", totals.inactive ?? 0, ")"] }), _jsxs(TabsTrigger, { value: "local", children: ["Lokaal (", totals.local ?? 0, ")"] }), providers.filter((p) => p.is_enabled).map((p) => (_jsxs(TabsTrigger, { value: p.provider, children: [p.label, " (", totals[p.provider] ?? 0, ")"] }, p.provider)))] }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsxs(Select, { value: customerClass, onValueChange: setCustomerClass, children: [_jsx(SelectTrigger, { className: "w-40", children: _jsx(SelectValue, { placeholder: "Klasse" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Alle klassen" }), (classData?.classes ?? []).map((c) => (_jsx(SelectItem, { value: c, children: c }, c)))] })] }), _jsxs(Select, { value: country, onValueChange: setCountry, children: [_jsx(SelectTrigger, { className: "w-36", children: _jsx(SelectValue, { placeholder: "Land" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Alle landen" }), countries.map((c) => (_jsx(SelectItem, { value: c, children: c }, c)))] })] })] }), activeFilterChips.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [activeFilterChips.map((chip) => (_jsxs("button", { type: "button", onClick: chip.clear, className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/50 px-3 py-1 text-xs hover:bg-muted", children: [chip.label, " ", _jsx(X, { className: "h-3 w-3" })] }, chip.key))), _jsx("button", { type: "button", onClick: () => { setSearch(""); setCustomerClass("__all__"); setCountry("__all__"); }, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx("div", { className: "text-sm text-muted-foreground", children: isLoading ? "Laden..." : `${customers.length} ${customers.length === 1 ? "klant" : "klanten"} gevonden` }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 6 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : customers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, children: _jsx(EmptyState, { onAdd: () => navigate({ to: "/klanten/nieuw" }) }) }) })) : customers.map((c) => {
116
143
  const secondary = c.commercial_name && c.commercial_name !== c.company_name
117
144
  ? c.commercial_name
118
145
  : c.city ?? c.email ?? null;
119
- return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => navigate({ to: "/klanten/$id", params: { id: c.id } }), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium truncate", children: c.company_name }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(MapPin, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", c.country] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsxs("span", { className: "inline-flex items-center gap-1.5 text-xs", children: [c.source === "floriday" ? _jsx(Cloud, { className: "h-3.5 w-3.5 text-muted-foreground" }) : _jsx(HardDrive, { className: "h-3.5 w-3.5 text-muted-foreground" }), c.source === "floriday" ? "Floriday" : "Lokaal"] }) }), _jsx(TableCell, { className: "py-3", children: _jsx(StatusPill, { active: c.is_active }) })] }, c.id));
146
+ return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => c.source === "local"
147
+ ? navigate({ to: "/klanten/$id", params: { id: c.id } })
148
+ : navigate({
149
+ to: "/klanten/groep/$groupKey",
150
+ params: { groupKey: c.group_key },
151
+ }), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium truncate", children: c.company_name }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(MapPin, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", c.country] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.source === "local" ? (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-xs", children: [_jsx(HardDrive, { className: "h-3.5 w-3.5 text-muted-foreground" }), c.source_label] })) : (_jsx("div", { className: "flex flex-wrap gap-1", children: c.provider_labels.map((lbl) => (_jsxs(Badge, { variant: "outline", className: "text-[10px] font-normal", children: [_jsx(Cloud, { className: "h-3 w-3 mr-1" }), lbl] }, lbl))) })) }), _jsx(TableCell, { className: "py-3", children: _jsx(StatusPill, { active: c.is_active }) })] }, c.key));
120
152
  }) })] }) }) })] }));
121
153
  }
122
154
  function StatCard({ icon, label, value, active, onClick, }) {
@@ -131,8 +163,8 @@ function StatusPill({ active }) {
131
163
  ? "bg-primary/10 text-primary"
132
164
  : "bg-muted text-muted-foreground"}`, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${active ? "bg-primary" : "bg-muted-foreground"}` }), active ? "Actief" : "Inactief"] }));
133
165
  }
134
- function EmptyState({ onSync, onAdd }) {
135
- return (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-muted flex items-center justify-center", children: _jsx(Users, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Geen klanten gevonden" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Pas je filters aan, synchroniseer met Floriday of voeg er handmatig \u00E9\u00E9n toe." })] }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: onSync, children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-2" }), " Synchroniseer Floriday"] }), _jsxs(Button, { size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }));
166
+ function EmptyState({ onAdd }) {
167
+ return (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-muted flex items-center justify-center", children: _jsx(Users, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Geen klanten gevonden" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Pas je filters aan of voeg handmatig een klant toe." })] }), _jsx("div", { className: "flex gap-2 mt-2", children: _jsxs(Button, { size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] }) })] }));
136
168
  }
137
169
  function relativeTime(iso) {
138
170
  const d = new Date(iso);