@flowselections/floriday-klanten-module 1.0.2 → 1.0.4

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.
@@ -0,0 +1,2 @@
1
+ export declare function KlantenOverzicht(): import("react").JSX.Element;
2
+ //# sourceMappingURL=KlantenOverzicht.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAiBA,wBAAgB,gBAAgB,gCAmT/B"}
@@ -0,0 +1,153 @@
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 { 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";
9
+ import { CustomerAvatar } from "../CustomerAvatar";
10
+ export function KlantenOverzicht() {
11
+ const navigate = useNavigate();
12
+ const qc = useQueryClient();
13
+ const [search, setSearch] = useState("");
14
+ const [customerClass, setCustomerClass] = useState("__all__");
15
+ const [country, setCountry] = useState("__all__");
16
+ const [view, setView] = useState("all");
17
+ const [sort, setSort] = useState({ col: "company_name", dir: "asc" });
18
+ const list = useServerFn(listCustomers);
19
+ const sync = useServerFn(syncFloridayCustomers);
20
+ 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
+ const { data: classData } = useQuery({
24
+ queryKey: ["customer-classes"],
25
+ queryFn: () => classesFn({ data: undefined }),
26
+ });
27
+ const { data: totalsData } = useQuery({
28
+ queryKey: ["customers", "totals"],
29
+ queryFn: () => list({
30
+ data: { search: "", customer_class: "", country: "", is_active: "all", source: "all" },
31
+ }),
32
+ });
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
+ }),
44
+ });
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]);
88
+ const countries = useMemo(() => {
89
+ const set = new Set();
90
+ all.forEach((c) => { if (c.country)
91
+ set.add(c.country); });
92
+ return Array.from(set).sort();
93
+ }, [all]);
94
+ const baseCustomers = data?.customers ?? [];
95
+ const customers = useMemo(() => {
96
+ let rows = baseCustomers;
97
+ if (country !== "__all__")
98
+ 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();
102
+ const cmp = av.localeCompare(bv);
103
+ return sort.dir === "asc" ? cmp : -cmp;
104
+ });
105
+ return sorted;
106
+ }, [baseCustomers, country, sort]);
107
+ const activeFilterChips = [];
108
+ if (search)
109
+ activeFilterChips.push({ key: "s", label: `Zoek: "${search}"`, clear: () => setSearch("") });
110
+ if (customerClass !== "__all__")
111
+ activeFilterChips.push({ key: "c", label: `Klasse: ${customerClass}`, clear: () => setCustomerClass("__all__") });
112
+ if (country !== "__all__")
113
+ activeFilterChips.push({ key: "co", label: `Land: ${country}`, clear: () => setCountry("__all__") });
114
+ 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) => {
116
+ const secondary = c.commercial_name && c.commercial_name !== c.company_name
117
+ ? c.commercial_name
118
+ : 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));
120
+ }) })] }) }) })] }));
121
+ }
122
+ function StatCard({ icon, label, value, active, onClick, }) {
123
+ return (_jsxs("button", { type: "button", onClick: onClick, className: `rounded-lg border bg-card p-4 text-left transition-all hover:shadow-sm ${active ? "border-primary ring-1 ring-primary/30" : "border-border"}`, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [icon, " ", label] }), _jsx("div", { className: "mt-1 text-2xl font-bold", children: value })] }));
124
+ }
125
+ function SortHeader({ label, col, sort, onClick, }) {
126
+ const isActive = sort.col === col;
127
+ return (_jsxs("button", { type: "button", onClick: () => onClick(col), className: `inline-flex items-center gap-1 hover:text-foreground ${isActive ? "text-foreground" : ""}`, children: [label, _jsx(ArrowUpDown, { className: "h-3 w-3 opacity-60" })] }));
128
+ }
129
+ function StatusPill({ active }) {
130
+ return (_jsxs("span", { className: `inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${active
131
+ ? "bg-primary/10 text-primary"
132
+ : "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
+ }
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"] })] })] }));
136
+ }
137
+ function relativeTime(iso) {
138
+ const d = new Date(iso);
139
+ const diff = Date.now() - d.getTime();
140
+ const sec = Math.floor(diff / 1000);
141
+ if (sec < 60)
142
+ return "zojuist";
143
+ const min = Math.floor(sec / 60);
144
+ if (min < 60)
145
+ return `${min} min geleden`;
146
+ const hr = Math.floor(min / 60);
147
+ if (hr < 24)
148
+ return `${hr} uur geleden`;
149
+ const day = Math.floor(hr / 24);
150
+ if (day < 7)
151
+ return `${day} dag${day === 1 ? "" : "en"} geleden`;
152
+ return d.toLocaleDateString("nl-NL");
153
+ }
@@ -1,5 +1,5 @@
1
1
  import type { FlowModule } from "@flowselections/core";
2
2
  export * from "./_core-safelist";
3
- export * from "./components/TemplatePage";
3
+ export { KlantenOverzicht } from "./components/klanten/KlantenOverzicht";
4
4
  export declare const myModule: FlowModule;
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAE1C,eAAO,MAAM,QAAQ,EAAE,UAYtB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAEzE,eAAO,MAAM,QAAQ,EAAE,UAYtB,CAAC"}
package/dist-lib/index.js CHANGED
@@ -16,14 +16,14 @@
16
16
  import { Users } from "lucide-react";
17
17
  import { CustomerFieldsCard } from "./components/settings/CustomerFieldsCard";
18
18
  export * from "./_core-safelist";
19
- export * from "./components/TemplatePage";
19
+ export { KlantenOverzicht } from "./components/klanten/KlantenOverzicht";
20
20
  export const myModule = {
21
21
  id: "customers",
22
22
  name: "Klanten",
23
23
  version: "1.0.0",
24
24
  nav: {
25
25
  label: "Klanten",
26
- href: "/klanten",
26
+ href: "/floriday-klanten",
27
27
  icon: Users,
28
28
  },
29
29
  settingsCards: [
@@ -45,8 +45,8 @@ export declare const listFiltersSchema: z.ZodObject<{
45
45
  }>>>;
46
46
  source: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
47
47
  local: "local";
48
- floriday: "floriday";
49
48
  all: "all";
49
+ floriday: "floriday";
50
50
  }>>>;
51
51
  }, z.core.$strip>;
52
52
  export type ListFiltersInput = z.infer<typeof listFiltersSchema>;
@@ -1561,7 +1561,7 @@ export declare const listCustomers: import("@tanstack/start-client-core").Requir
1561
1561
  customer_class: string;
1562
1562
  country: string;
1563
1563
  is_active: "active" | "all" | "inactive";
1564
- source: "local" | "floriday" | "all";
1564
+ source: "local" | "all" | "floriday";
1565
1565
  }, Promise<{
1566
1566
  customers: {
1567
1567
  address: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/floriday-klanten-module",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",