@flowselections/floriday-klanten-module 1.0.11 → 1.0.13
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.
- package/dist-lib/components/klanten/DiscountsTab.d.ts +4 -0
- package/dist-lib/components/klanten/DiscountsTab.d.ts.map +1 -0
- package/dist-lib/components/klanten/DiscountsTab.js +92 -0
- package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/GroupedKlantDetail.js +5 -2
- package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDetail.js +3 -2
- package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenOverzicht.js +83 -23
- package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts +10 -0
- package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts.map +1 -0
- package/dist-lib/components/klanten/LocalDiscountFormDialog.js +107 -0
- package/dist-lib/components/klanten/LocalDiscountsTab.d.ts +4 -0
- package/dist-lib/components/klanten/LocalDiscountsTab.d.ts.map +1 -0
- package/dist-lib/components/klanten/LocalDiscountsTab.js +101 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +1788 -107
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +1788 -107
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +1788 -107
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +1819 -104
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting.functions.d.ts +5362 -319
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-analytics.functions.d.ts +1788 -107
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customers.functions.d.ts +11767 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/delivery-locations.functions.d.ts +6724 -0
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.d.ts +6724 -0
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-discounts.functions.d.ts +21974 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-discounts.functions.js +83 -0
- package/dist-lib/lib/local-discounts.functions.d.ts +76898 -0
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -0
- package/dist-lib/lib/local-discounts.functions.js +185 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts +10984 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -0
- package/dist-lib/lib/overdue-invoices.functions.js +72 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscountsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/DiscountsTab.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,YAAY,CAAC,EAAE,mBAAmB,EAAE,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAA;CAAE,+BAmJpF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
+
import { Card, CardContent, Badge, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
6
|
+
import { Info, Percent, FileText, Users } from "lucide-react";
|
|
7
|
+
import { listCustomerDiscounts, } from "../../lib/floriday-discounts.functions";
|
|
8
|
+
import { EmptyState } from "./KlantDetailLayout";
|
|
9
|
+
const SOURCE_LABEL = {
|
|
10
|
+
agreement: "Contract",
|
|
11
|
+
sales_strategy: "Sales Strategy",
|
|
12
|
+
price_group: "Prijsgroep",
|
|
13
|
+
};
|
|
14
|
+
const SOURCE_ICON = {
|
|
15
|
+
agreement: _jsx(FileText, { className: "h-3 w-3" }),
|
|
16
|
+
sales_strategy: _jsx(Percent, { className: "h-3 w-3" }),
|
|
17
|
+
price_group: _jsx(Users, { className: "h-3 w-3" }),
|
|
18
|
+
};
|
|
19
|
+
function fmtMoney(v, currency = "EUR") {
|
|
20
|
+
if (v == null)
|
|
21
|
+
return "—";
|
|
22
|
+
try {
|
|
23
|
+
return new Intl.NumberFormat("nl-NL", { style: "currency", currency }).format(v);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return `${v.toFixed(2)} ${currency}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function fmtDiscount(row) {
|
|
30
|
+
if (row.discount_type == null || row.discount_value == null)
|
|
31
|
+
return "—";
|
|
32
|
+
return row.discount_type === "percentage"
|
|
33
|
+
? `${row.discount_value}%`
|
|
34
|
+
: fmtMoney(row.discount_value, row.currency);
|
|
35
|
+
}
|
|
36
|
+
function fmtDate(v) {
|
|
37
|
+
if (!v)
|
|
38
|
+
return "—";
|
|
39
|
+
try {
|
|
40
|
+
return new Date(v).toLocaleDateString("nl-NL");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isActiveOn(row, date) {
|
|
47
|
+
if (row.valid_from && row.valid_from > date)
|
|
48
|
+
return false;
|
|
49
|
+
if (row.valid_until && row.valid_until < date)
|
|
50
|
+
return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function DiscountsTab({ buyerOrganizationId }) {
|
|
54
|
+
const fn = useServerFn(listCustomerDiscounts);
|
|
55
|
+
const { data, isLoading, error } = useQuery({
|
|
56
|
+
queryKey: ["floriday-discounts", buyerOrganizationId],
|
|
57
|
+
queryFn: () => fn({ data: { buyerOrganizationId } }),
|
|
58
|
+
});
|
|
59
|
+
const [source, setSource] = useState("all");
|
|
60
|
+
const [search, setSearch] = useState("");
|
|
61
|
+
const [onlyActive, setOnlyActive] = useState(true);
|
|
62
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
63
|
+
const filtered = useMemo(() => {
|
|
64
|
+
const rows = data?.rows ?? [];
|
|
65
|
+
return rows.filter((r) => {
|
|
66
|
+
if (source !== "all" && r.source !== source)
|
|
67
|
+
return false;
|
|
68
|
+
if (onlyActive && !isActiveOn(r, today))
|
|
69
|
+
return false;
|
|
70
|
+
if (search) {
|
|
71
|
+
const q = search.toLowerCase();
|
|
72
|
+
const hay = [r.trade_item_name, r.trade_item_id, r.product_group_id].filter(Boolean).join(" ").toLowerCase();
|
|
73
|
+
if (!hay.includes(q))
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
}, [data, source, search, onlyActive, today]);
|
|
79
|
+
if (isLoading)
|
|
80
|
+
return _jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: "Kortingen laden\u2026" }) });
|
|
81
|
+
if (error) {
|
|
82
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 text-sm text-destructive", children: ["Kortingen konden niet worden geladen: ", error.message] }) }));
|
|
83
|
+
}
|
|
84
|
+
const s = data?.summary;
|
|
85
|
+
const rows = data?.rows ?? [];
|
|
86
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [_jsx(SummaryCard, { label: "Contracten", value: s?.agreements ?? 0, icon: _jsx(FileText, { className: "h-4 w-4" }) }), _jsx(SummaryCard, { label: "Prijsregels", value: s?.sales_strategies ?? 0, icon: _jsx(Percent, { className: "h-4 w-4" }) }), _jsx(SummaryCard, { label: "Prijsgroep-regels", value: s?.price_groups ?? 0, icon: _jsx(Users, { className: "h-4 w-4" }) }), _jsx(SummaryCard, { label: "Laatst gesynchroniseerd", value: s?.last_fetched_at ? new Date(s.last_fetched_at).toLocaleString("nl-NL") : "—", icon: _jsx(Info, { className: "h-4 w-4" }), small: true })] }), rows.length === 0 ? (_jsx(EmptyState, { message: "Nog geen kortingsdata beschikbaar. Floriday moet de scopes " +
|
|
87
|
+
"'sales-strategy:read' en 'agreement:read' voor onze Client ID activeren; " +
|
|
88
|
+
"daarna vult de authenticatiemodule deze cache automatisch." })) : (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Input, { placeholder: "Zoek op artikel of ID\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "max-w-xs" }), _jsxs(Select, { value: source, onValueChange: (v) => setSource(v), children: [_jsx(SelectTrigger, { className: "w-44", children: _jsx(SelectValue, { placeholder: "Bron" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle bronnen" }), _jsx(SelectItem, { value: "agreement", children: "Contract" }), _jsx(SelectItem, { value: "sales_strategy", children: "Sales Strategy" }), _jsx(SelectItem, { value: "price_group", children: "Prijsgroep" })] })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-muted-foreground ml-auto", children: [_jsx("input", { type: "checkbox", checked: onlyActive, onChange: (e) => setOnlyActive(e.target.checked) }), "Alleen actief vandaag"] })] }), _jsx("div", { className: "rounded-md border overflow-x-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikel / Groep" }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { className: "text-right", children: "Basisprijs" }), _jsx(TableHead, { className: "text-right", children: "Korting" }), _jsx(TableHead, { className: "text-right", children: "Netto" }), _jsx(TableHead, { children: "Geldig" }), _jsx(TableHead, { className: "text-right", children: "Min. hvh" })] }) }), _jsx(TableBody, { children: filtered.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 7, className: "text-center text-muted-foreground py-6", children: "Geen kortingen die aan de filters voldoen." }) })) : filtered.map((r, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: r.trade_item_name ?? r.trade_item_id ?? r.product_group_id ?? "—" }), r.trade_item_name && (_jsx("div", { className: "text-xs text-muted-foreground font-mono truncate max-w-[280px]", children: r.trade_item_id }))] }), _jsx(TableCell, { children: _jsxs(Badge, { variant: "secondary", className: "gap-1", children: [SOURCE_ICON[r.source], SOURCE_LABEL[r.source]] }) }), _jsx(TableCell, { className: "text-right tabular-nums", children: fmtMoney(r.base_price, r.currency) }), _jsx(TableCell, { className: "text-right tabular-nums", children: fmtDiscount(r) }), _jsx(TableCell, { className: "text-right tabular-nums font-medium", children: fmtMoney(r.net_price, r.currency) }), _jsxs(TableCell, { className: "text-sm text-muted-foreground", children: [fmtDate(r.valid_from), " \u2013 ", fmtDate(r.valid_until)] }), _jsx(TableCell, { className: "text-right tabular-nums", children: r.min_quantity || "—" })] }, `${r.source_id}-${r.trade_item_id}-${i}`))) })] }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wijzigingen doe je in Floriday. Deze module leest de gesynchroniseerde kortingscache alleen." })] }) }))] }));
|
|
89
|
+
}
|
|
90
|
+
function SummaryCard({ label, value, icon, small, }) {
|
|
91
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { className: "p-4", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs uppercase text-muted-foreground", children: [icon, label] }), _jsx("div", { className: small ? "text-sm mt-1" : "text-2xl font-semibold mt-1", children: value })] }) }));
|
|
92
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BAwNpE"}
|
|
@@ -7,6 +7,7 @@ import { Card, CardContent, Badge, Button, Tabs, TabsList, TabsTrigger, TabsCont
|
|
|
7
7
|
import { ArrowLeft, Mail, Phone } from "lucide-react";
|
|
8
8
|
import { getGroupedCustomerDetail, } from "../../lib/external-customers.functions";
|
|
9
9
|
import { ProfileCard, KpiStrip, BadgeList, EmptyState, adaptGroupedToProfile, } from "../../components/klanten/KlantDetailLayout";
|
|
10
|
+
import { DiscountsTab } from "../../components/klanten/DiscountsTab";
|
|
10
11
|
export function GroupedKlantDetail({ groupKey }) {
|
|
11
12
|
const navigate = useNavigate();
|
|
12
13
|
const get = useServerFn(getGroupedCustomerDetail);
|
|
@@ -24,6 +25,8 @@ export function GroupedKlantDetail({ groupKey }) {
|
|
|
24
25
|
const providerMap = new Map(data.providers.map((p) => [p.provider, p]));
|
|
25
26
|
const sources = g.sources ?? [];
|
|
26
27
|
const profile = adaptGroupedToProfile(g);
|
|
28
|
+
const floridaySource = sources.find((s) => s.provider === "floriday");
|
|
29
|
+
const floridayBuyerId = floridaySource?.external_id ?? null;
|
|
27
30
|
const contacts = collectContacts(sources, providerMap);
|
|
28
31
|
const locations = collectLocations(sources, providerMap);
|
|
29
32
|
const segments = collectArray(sources, ["segments"]);
|
|
@@ -40,13 +43,13 @@ export function GroupedKlantDetail({ groupKey }) {
|
|
|
40
43
|
provider: providerMap.get(s.provider)?.label ?? s.provider,
|
|
41
44
|
}))
|
|
42
45
|
.sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
|
|
43
|
-
return (_jsxs("main", { className: "p-6
|
|
46
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug naar klanten"] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsx(KpiStrip, { counts: {
|
|
44
47
|
contacts: contacts.length,
|
|
45
48
|
locations: locations.length,
|
|
46
49
|
assortment: 0,
|
|
47
50
|
orders: 0,
|
|
48
51
|
lastSync: g.fetched_at,
|
|
49
|
-
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [(segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: locations.length === 0 ? (_jsx(EmptyState, { message: "Geen afleverlocaties beschikbaar via de koppeling." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsx(TableCell, { className: "text-sm", children: [l.postal_code, l.country].filter(Boolean).join(" · ") }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })) }), _jsx(TabsContent, { value: "assortment", children: _jsx(EmptyState, { message: "Assortiment is beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "orders", children: _jsx(EmptyState, { message: "Bestellingen zijn beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "trade", children: _jsx(EmptyState, { message: "Handelsinstellingen zijn niet beschikbaar via deze koppeling." }) }), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: activity.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (activity.map((a) => (_jsxs("div", { className: "flex items-center justify-between text-sm border-l-2 border-primary pl-3 py-1", children: [_jsxs("span", { children: ["Bijgewerkt via ", _jsx("strong", { children: a.provider })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(a.created_at).toLocaleString("nl-NL") })] }, a.id)))) }) }) }), g.is_merged && sources.length > 1 && (_jsx(TabsContent, { value: "sources", children: _jsx(CompareCard, { sources: sources, providerMap: providerMap }) }))] })] })] })] }));
|
|
52
|
+
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [(segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: locations.length === 0 ? (_jsx(EmptyState, { message: "Geen afleverlocaties beschikbaar via de koppeling." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsx(TableCell, { className: "text-sm", children: [l.postal_code, l.country].filter(Boolean).join(" · ") }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })) }), _jsx(TabsContent, { value: "assortment", children: _jsx(EmptyState, { message: "Assortiment is beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "orders", children: _jsx(EmptyState, { message: "Bestellingen zijn beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "trade", children: _jsx(EmptyState, { message: "Handelsinstellingen zijn niet beschikbaar via deze koppeling." }) }), floridayBuyerId && (_jsx(TabsContent, { value: "discounts", children: _jsx(DiscountsTab, { buyerOrganizationId: floridayBuyerId }) })), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: activity.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (activity.map((a) => (_jsxs("div", { className: "flex items-center justify-between text-sm border-l-2 border-primary pl-3 py-1", children: [_jsxs("span", { children: ["Bijgewerkt via ", _jsx("strong", { children: a.provider })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(a.created_at).toLocaleString("nl-NL") })] }, a.id)))) }) }) }), g.is_merged && sources.length > 1 && (_jsx(TabsContent, { value: "sources", children: _jsx(CompareCard, { sources: sources, providerMap: providerMap }) }))] })] })] })] }));
|
|
50
53
|
}
|
|
51
54
|
function collectContacts(sources, providerMap) {
|
|
52
55
|
const out = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAuBA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAuPjD"}
|
|
@@ -9,6 +9,7 @@ import { getCustomer, deleteCustomer, updateCustomer } from "../../lib/customers
|
|
|
9
9
|
import { ProfileCard, KpiStrip, BadgeList, EmptyState, ActivityItem, adaptLocalToProfile, } from "../../components/klanten/KlantDetailLayout";
|
|
10
10
|
import { DeliveryLocationsTab } from "../../components/klanten/DeliveryLocationsTab";
|
|
11
11
|
import { CustomerAnalyticsTab } from "../../components/klanten/CustomerAnalyticsTab";
|
|
12
|
+
import { LocalDiscountsTab } from "../../components/klanten/LocalDiscountsTab";
|
|
12
13
|
export function KlantDetail({ id }) {
|
|
13
14
|
const navigate = useNavigate();
|
|
14
15
|
const qc = useQueryClient();
|
|
@@ -33,11 +34,11 @@ export function KlantDetail({ id }) {
|
|
|
33
34
|
if (!data)
|
|
34
35
|
return _jsx("main", { className: "p-6", children: "Klant niet gevonden" });
|
|
35
36
|
const c = data.customer;
|
|
36
|
-
return (_jsxs("main", { className: "p-6
|
|
37
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug naar klanten"] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts) }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsx(KpiStrip, { counts: {
|
|
37
38
|
contacts: data.contacts.length,
|
|
38
39
|
locations: data.delivery_locations.length,
|
|
39
40
|
assortment: data.assortment.length,
|
|
40
41
|
orders: data.orders.length,
|
|
41
42
|
lastSync: c.last_synced_at,
|
|
42
|
-
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex flex-wrap h-auto gap-1 mb-4", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsx(TabsTrigger, { value: "dashboard", children: "Analyses" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", data.contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", data.delivery_locations.length, ")"] }), _jsxs(TabsTrigger, { value: "assortment", children: ["Assortiment (", data.assortment.length, ")"] }), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" })] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [c.bio && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bio" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.bio })] })), (c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Profiel" }) }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: c.segments }), _jsx(BadgeList, { label: "Handelsvorm", items: c.trade_forms }), _jsx(BadgeList, { label: "Afzetmarkten", items: c.markets }), _jsx(BadgeList, { label: "Productgroepen", items: c.product_groups })] })] })) : null, (c.payment_methods?.length ?? 0) > 0 && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Financi\u00EBle afhandeling" }) }), _jsx(CardContent, { children: _jsx(BadgeList, { label: "Betaalmethoden", items: c.payment_methods }) })] })), c.notes && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Notities" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.notes })] })), !c.bio && !c.notes && !(c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) && !(c.payment_methods?.length) && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: data.contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen gesynchroniseerd." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: data.contacts.map((p) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "secondary", children: "Inactief" })] }) }, p.id))) })) }), _jsx(TabsContent, { value: "trade", children: data.trade_settings.length === 0 ? (_jsx(EmptyState, { message: "Handelsinstellingen zijn (nog) niet beschikbaar via de Suppliers API voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.trade_settings.map((s) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: s.is_active ? "default" : "secondary", children: s.is_active ? "Actief" : "Niet actief" }), _jsx("span", { className: "text-sm", children: s.setting_label })] }, s.id))) }) })) }), _jsx(TabsContent, { value: "locations", children: _jsx(DeliveryLocationsTab, { customerId: id }) }), _jsx(TabsContent, { value: "dashboard", children: _jsx(CustomerAnalyticsTab, { customerId: id }) }), _jsx(TabsContent, { value: "assortment", children: data.assortment.length === 0 ? (_jsx(EmptyState, { message: "Geen geselecteerd assortiment voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikelnummer" }), _jsx(TableHead, { children: "Product" }), _jsx(TableHead, { children: "Groep" }), _jsx(TableHead, { children: "Prijs" })] }) }), _jsx(TableBody, { children: data.assortment.map((a) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: a.article_number || "—" }), _jsx(TableCell, { children: a.product_name }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: a.product_group || "—" }), _jsx(TableCell, { children: a.price != null ? `€ ${Number(a.price).toFixed(2)}${a.unit ? ` / ${a.unit}` : ""}` : "—" })] }, a.id))) })] }) }) })) }), _jsx(TabsContent, { value: "orders", children: _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Totaal" })] }) }), _jsx(TableBody, { children: data.orders.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center py-6 text-muted-foreground", children: "Geen bestellingen" }) })) : data.orders.map((o) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: o.order_number }), _jsx(TableCell, { children: o.order_date }), _jsx(TableCell, { children: _jsx(Badge, { children: o.status }) }), _jsx(TableCell, { children: o.total })] }, o.id))) })] }) }) }) }), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.audit.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (data.audit.map((a) => _jsx(ActivityItem, { entry: a }, a.id))) }) }) })] })] })] })] }));
|
|
43
|
+
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex flex-wrap h-auto gap-1 mb-4", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsx(TabsTrigger, { value: "dashboard", children: "Analyses" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", data.contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", data.delivery_locations.length, ")"] }), _jsxs(TabsTrigger, { value: "assortment", children: ["Assortiment (", data.assortment.length, ")"] }), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" })] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [c.bio && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bio" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.bio })] })), (c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Profiel" }) }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: c.segments }), _jsx(BadgeList, { label: "Handelsvorm", items: c.trade_forms }), _jsx(BadgeList, { label: "Afzetmarkten", items: c.markets }), _jsx(BadgeList, { label: "Productgroepen", items: c.product_groups })] })] })) : null, (c.payment_methods?.length ?? 0) > 0 && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Financi\u00EBle afhandeling" }) }), _jsx(CardContent, { children: _jsx(BadgeList, { label: "Betaalmethoden", items: c.payment_methods }) })] })), c.notes && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Notities" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.notes })] })), !c.bio && !c.notes && !(c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) && !(c.payment_methods?.length) && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: data.contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen gesynchroniseerd." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: data.contacts.map((p) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "secondary", children: "Inactief" })] }) }, p.id))) })) }), _jsx(TabsContent, { value: "trade", children: data.trade_settings.length === 0 ? (_jsx(EmptyState, { message: "Handelsinstellingen zijn (nog) niet beschikbaar via de Suppliers API voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.trade_settings.map((s) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: s.is_active ? "default" : "secondary", children: s.is_active ? "Actief" : "Niet actief" }), _jsx("span", { className: "text-sm", children: s.setting_label })] }, s.id))) }) })) }), _jsx(TabsContent, { value: "locations", children: _jsx(DeliveryLocationsTab, { customerId: id }) }), _jsx(TabsContent, { value: "dashboard", children: _jsx(CustomerAnalyticsTab, { customerId: id }) }), _jsx(TabsContent, { value: "assortment", children: data.assortment.length === 0 ? (_jsx(EmptyState, { message: "Geen geselecteerd assortiment voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikelnummer" }), _jsx(TableHead, { children: "Product" }), _jsx(TableHead, { children: "Groep" }), _jsx(TableHead, { children: "Prijs" })] }) }), _jsx(TableBody, { children: data.assortment.map((a) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: a.article_number || "—" }), _jsx(TableCell, { children: a.product_name }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: a.product_group || "—" }), _jsx(TableCell, { children: a.price != null ? `€ ${Number(a.price).toFixed(2)}${a.unit ? ` / ${a.unit}` : ""}` : "—" })] }, a.id))) })] }) }) })) }), _jsx(TabsContent, { value: "orders", children: _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Totaal" })] }) }), _jsx(TableBody, { children: data.orders.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center py-6 text-muted-foreground", children: "Geen bestellingen" }) })) : data.orders.map((o) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: o.order_number }), _jsx(TableCell, { children: o.order_date }), _jsx(TableCell, { children: _jsx(Badge, { children: o.status }) }), _jsx(TableCell, { children: o.total })] }, o.id))) })] }) }) }) }), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.audit.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (data.audit.map((a) => _jsx(ActivityItem, { entry: a }, a.id))) }) }) }), _jsx(TabsContent, { value: "discounts", children: _jsx(LocalDiscountsTab, { customerId: id }) })] })] })] })] }));
|
|
43
44
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAwCA,wBAAgB,gBAAgB,gCAwZ/B"}
|
|
@@ -3,10 +3,12 @@ import { useNavigate } from "@tanstack/react-router";
|
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
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,
|
|
7
|
-
import { Plus, Search, Network, Users, UserCheck, X, ArrowUpDown, Mail, MapPin, } from "lucide-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, AlertCircle, X, ArrowUpDown, Mail, MapPin, ChevronLeft, ChevronRight, } from "lucide-react";
|
|
8
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
8
9
|
import { listCustomers, listCustomerClasses } from "../../lib/customers.functions";
|
|
9
10
|
import { listExternalCustomers } from "../../lib/external-customers.functions";
|
|
11
|
+
import { listOverdueBalances } from "../../lib/overdue-invoices.functions";
|
|
10
12
|
import { CustomerAvatar } from "../CustomerAvatar";
|
|
11
13
|
import { formatCountry } from "./KlantDetailLayout";
|
|
12
14
|
export function KlantenOverzicht() {
|
|
@@ -15,11 +17,13 @@ export function KlantenOverzicht() {
|
|
|
15
17
|
const [customerClass, setCustomerClass] = useState("__all__");
|
|
16
18
|
const [country, setCountry] = useState("__all__");
|
|
17
19
|
const [view, setView] = useState("all");
|
|
18
|
-
const [showInactive, setShowInactive] = useState(false);
|
|
19
20
|
const [sort, setSort] = useState({ col: "company_name", dir: "asc" });
|
|
21
|
+
const [page, setPage] = useState(1);
|
|
22
|
+
const [pageSize, setPageSize] = useState(25);
|
|
20
23
|
const list = useServerFn(listCustomers);
|
|
21
24
|
const listExternal = useServerFn(listExternalCustomers);
|
|
22
25
|
const classesFn = useServerFn(listCustomerClasses);
|
|
26
|
+
const overdueFn = useServerFn(listOverdueBalances);
|
|
23
27
|
const { data: classData } = useQuery({
|
|
24
28
|
queryKey: ["customer-classes"],
|
|
25
29
|
queryFn: () => classesFn({ data: undefined }),
|
|
@@ -34,9 +38,16 @@ export function KlantenOverzicht() {
|
|
|
34
38
|
queryKey: ["customers-external"],
|
|
35
39
|
queryFn: () => listExternal({ data: { status: "all" } }),
|
|
36
40
|
});
|
|
41
|
+
const { data: overdueData } = useQuery({
|
|
42
|
+
queryKey: ["overdue-balances"],
|
|
43
|
+
queryFn: () => overdueFn(),
|
|
44
|
+
staleTime: 60000,
|
|
45
|
+
});
|
|
37
46
|
const isLoading = loadingLocal || loadingExternal;
|
|
38
47
|
const providers = externalData?.providers ?? [];
|
|
39
48
|
const allRows = useMemo(() => {
|
|
49
|
+
const tw = overdueData?.by_provider.twinfield ?? {};
|
|
50
|
+
const ex = overdueData?.by_provider.exact ?? {};
|
|
40
51
|
const local = (localData?.customers ?? []).map((c) => ({
|
|
41
52
|
key: `local-${c.id}`,
|
|
42
53
|
id: c.id,
|
|
@@ -52,9 +63,24 @@ export function KlantenOverzicht() {
|
|
|
52
63
|
customer_class: c.customer_class ?? null,
|
|
53
64
|
logo_url: c.logo_url ?? null,
|
|
54
65
|
is_active: !!c.is_active,
|
|
66
|
+
overdue: null,
|
|
55
67
|
}));
|
|
56
68
|
const ext = (externalData?.customers ?? []).map((c) => {
|
|
57
69
|
const provs = c.providers ?? [];
|
|
70
|
+
let overdue = null;
|
|
71
|
+
for (const s of (c.sources ?? [])) {
|
|
72
|
+
const hit = s.provider === "twinfield" ? tw[s.external_id]
|
|
73
|
+
: s.provider === "exact" ? ex[s.external_id]
|
|
74
|
+
: undefined;
|
|
75
|
+
if (!hit)
|
|
76
|
+
continue;
|
|
77
|
+
if (!overdue)
|
|
78
|
+
overdue = { count: 0, total: 0, oldest_due: hit.oldest_due };
|
|
79
|
+
overdue.count += hit.count;
|
|
80
|
+
overdue.total += hit.total;
|
|
81
|
+
if (hit.oldest_due < overdue.oldest_due)
|
|
82
|
+
overdue.oldest_due = hit.oldest_due;
|
|
83
|
+
}
|
|
58
84
|
return {
|
|
59
85
|
key: `grp-${c.group_key}`,
|
|
60
86
|
id: null,
|
|
@@ -70,14 +96,17 @@ export function KlantenOverzicht() {
|
|
|
70
96
|
customer_class: null,
|
|
71
97
|
logo_url: c.logo_url,
|
|
72
98
|
is_active: c.is_active,
|
|
99
|
+
overdue,
|
|
73
100
|
};
|
|
74
101
|
});
|
|
75
102
|
return [...local, ...ext];
|
|
76
|
-
}, [localData, externalData]);
|
|
103
|
+
}, [localData, externalData, overdueData]);
|
|
77
104
|
const totals = useMemo(() => {
|
|
78
105
|
const t = {
|
|
79
106
|
all: allRows.length,
|
|
80
107
|
active: allRows.filter((c) => c.is_active).length,
|
|
108
|
+
inactive: allRows.filter((c) => !c.is_active).length,
|
|
109
|
+
overdue: allRows.filter((c) => (c.overdue?.count ?? 0) > 0).length,
|
|
81
110
|
};
|
|
82
111
|
return t;
|
|
83
112
|
}, [allRows]);
|
|
@@ -91,8 +120,10 @@ export function KlantenOverzicht() {
|
|
|
91
120
|
let rows = allRows;
|
|
92
121
|
if (view === "active")
|
|
93
122
|
rows = rows.filter((c) => c.is_active);
|
|
94
|
-
if (
|
|
95
|
-
rows = rows.filter((c) => c.is_active);
|
|
123
|
+
if (view === "inactive")
|
|
124
|
+
rows = rows.filter((c) => !c.is_active);
|
|
125
|
+
if (view === "overdue")
|
|
126
|
+
rows = rows.filter((c) => (c.overdue?.count ?? 0) > 0);
|
|
96
127
|
if (customerClass !== "__all__")
|
|
97
128
|
rows = rows.filter((c) => c.customer_class === customerClass);
|
|
98
129
|
if (country !== "__all__")
|
|
@@ -109,30 +140,49 @@ export function KlantenOverzicht() {
|
|
|
109
140
|
const cmp = av.localeCompare(bv);
|
|
110
141
|
return sort.dir === "asc" ? cmp : -cmp;
|
|
111
142
|
});
|
|
112
|
-
}, [allRows, view, customerClass, country, search, sort
|
|
143
|
+
}, [allRows, view, customerClass, country, search, sort]);
|
|
144
|
+
const total = customers.length;
|
|
145
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
146
|
+
const currentPage = Math.min(page, totalPages);
|
|
147
|
+
const startIdx = total === 0 ? 0 : (currentPage - 1) * pageSize;
|
|
148
|
+
const endIdx = Math.min(startIdx + pageSize, total);
|
|
149
|
+
const pageRows = customers.slice(startIdx, endIdx);
|
|
150
|
+
const resetPage = () => setPage(1);
|
|
113
151
|
const activeFilterChips = [];
|
|
114
152
|
if (search)
|
|
115
|
-
activeFilterChips.push({ key: "s", label: `Zoek: "${search}"`, clear: () => setSearch("") });
|
|
153
|
+
activeFilterChips.push({ key: "s", label: `Zoek: "${search}"`, clear: () => { setSearch(""); resetPage(); } });
|
|
116
154
|
if (customerClass !== "__all__")
|
|
117
|
-
activeFilterChips.push({ key: "c", label: `Klasse: ${customerClass}`, clear: () => setCustomerClass("__all__") });
|
|
155
|
+
activeFilterChips.push({ key: "c", label: `Klasse: ${customerClass}`, clear: () => { setCustomerClass("__all__"); resetPage(); } });
|
|
118
156
|
if (country !== "__all__")
|
|
119
|
-
activeFilterChips.push({ key: "co", label: `Land: ${country}`, clear: () => setCountry("__all__") });
|
|
157
|
+
activeFilterChips.push({ key: "co", label: `Land: ${country}`, clear: () => { setCountry("__all__"); resetPage(); } });
|
|
120
158
|
const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
|
|
121
159
|
const lastUpdated = externalData?.last_fetched_at ?? null;
|
|
122
|
-
return (_jsxs("main", { className: "p-6 space-y-5
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
160
|
+
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/klanten/netwerk" }), children: [_jsx(Network, { className: "h-4 w-4 mr-2" }), " Klanten toevoegen uit netwerk"] }), _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-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: "Klanten met minimaal \u00E9\u00E9n bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: "Klanten zonder omzet in de afgelopen 12 maanden.", active: view === "inactive", onClick: () => { setView("inactive"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(AlertCircle, { className: "h-5 w-5" }), tint: "destructive", label: "Achterstallige betalingen", value: totals.overdue, tooltip: (overdueData
|
|
161
|
+
? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen\nBron: Twinfield & Exact · bijgewerkt ${relativeTime(overdueData.as_of)}`
|
|
162
|
+
: "Klanten met openstaande facturen waarvan de vervaldatum is verstreken."), active: view === "overdue", onClick: () => { setView("overdue"); resetPage(); } })] }), _jsxs(Card, { children: [_jsx("div", { className: "border-b px-4 pt-3", children: _jsx(Tabs, { value: view, onValueChange: (v) => { setView(v); resetPage(); }, children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.all ?? 0 })] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.active ?? 0 })] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.inactive ?? 0 })] }), _jsxs(TabsTrigger, { value: "overdue", children: ["Achterstallig ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.overdue ?? 0 })] })] }) }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap p-4 border-b", 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); resetPage(); }, className: "pl-9" })] }), _jsxs(Select, { value: customerClass, onValueChange: (v) => { setCustomerClass(v); resetPage(); }, 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: (v) => { setCountry(v); resetPage(); }, 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 px-4 py-2 border-b bg-muted/30", 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-card 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__"); resetPage(); }, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _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: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 5 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, children: _jsx(EmptyState, { onAdd: () => navigate({ to: "/klanten/nieuw" }) }) }) })) : pageRows.map((c) => {
|
|
163
|
+
const secondary = c.commercial_name && c.commercial_name !== c.company_name
|
|
164
|
+
? c.commercial_name
|
|
165
|
+
: c.city ?? c.email ?? null;
|
|
166
|
+
return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => c.source === "local"
|
|
167
|
+
? navigate({ to: "/klanten/$id", params: { id: c.id } })
|
|
168
|
+
: navigate({
|
|
169
|
+
to: "/klanten/groep/$groupKey",
|
|
170
|
+
params: { groupKey: c.group_key },
|
|
171
|
+
}), 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: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.overdue && c.overdue.count > 0 && (_jsxs("span", { title: `${c.overdue.count} openstaande factu${c.overdue.count === 1 ? "ur" : "ren"} · oudste vervallen op ${c.overdue.oldest_due}`, className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 text-destructive px-2 py-0.5 text-[11px] font-medium shrink-0", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", formatEuro(c.overdue.total), " achterstallig"] }))] }), 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" }), " ", formatCountry(c.country)] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsx(StatusPill, { active: c.is_active }) })] }, c.key));
|
|
172
|
+
}) })] }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3 border-t text-sm flex-wrap", children: [_jsx("span", { className: "text-muted-foreground", children: isLoading ? "Laden..." : total === 0 ? "0 klanten" : `${startIdx + 1}–${endIdx} van ${total}` }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Per pagina" }), _jsxs(Select, { value: String(pageSize), onValueChange: (v) => { setPageSize(Number(v)); setPage(1); }, children: [_jsx(SelectTrigger, { className: "w-20 h-8", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "25", children: "25" }), _jsx(SelectItem, { value: "50", children: "50" }), _jsx(SelectItem, { value: "100", children: "100" })] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.max(1, p - 1)), disabled: currentPage <= 1, children: [_jsx(ChevronLeft, { className: "h-4 w-4 mr-1" }), " Vorige"] }), _jsxs("span", { className: "text-muted-foreground", children: [currentPage, " / ", totalPages] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.min(totalPages, p + 1)), disabled: currentPage >= totalPages, children: ["Volgende ", _jsx(ChevronRight, { className: "h-4 w-4 ml-1" })] })] })] })] })] }) }));
|
|
133
173
|
}
|
|
134
|
-
function StatCard({ icon, label, value, active, onClick, }) {
|
|
135
|
-
|
|
174
|
+
function StatCard({ icon, label, value, active, onClick, tooltip, tint = "muted", disabled = false, }) {
|
|
175
|
+
const tintCls = {
|
|
176
|
+
muted: "bg-muted text-muted-foreground",
|
|
177
|
+
success: "bg-success/10 text-success",
|
|
178
|
+
warning: "bg-warning/10 text-warning",
|
|
179
|
+
destructive: "bg-destructive/10 text-destructive",
|
|
180
|
+
primary: "bg-primary/10 text-primary",
|
|
181
|
+
}[tint];
|
|
182
|
+
const button = (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled, className: `w-full rounded-lg border bg-card p-4 text-left transition-all hover:shadow-sm disabled:cursor-not-allowed disabled:opacity-70 ${active ? "border-primary ring-1 ring-primary/30" : "border-border"}`, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-flex h-8 w-8 items-center justify-center rounded-md ${tintCls}`, children: icon }), _jsx("span", { className: "text-sm font-medium text-foreground truncate", children: label })] }), _jsx("div", { className: "mt-3 text-3xl font-bold tabular-nums leading-none", children: value === null ? _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) : value })] }));
|
|
183
|
+
if (!tooltip)
|
|
184
|
+
return button;
|
|
185
|
+
return (_jsxs(TooltipPrimitive.Root, { children: [_jsx(TooltipPrimitive.Trigger, { asChild: true, children: button }), _jsx(TooltipPrimitive.Portal, { children: _jsxs(TooltipPrimitive.Content, { side: "bottom", sideOffset: 6, className: "z-50 max-w-xs rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md", children: [tooltip, _jsx(TooltipPrimitive.Arrow, { className: "fill-popover" })] }) })] }));
|
|
136
186
|
}
|
|
137
187
|
function SortHeader({ label, col, sort, onClick, }) {
|
|
138
188
|
const isActive = sort.col === col;
|
|
@@ -147,6 +197,8 @@ function EmptyState({ onAdd }) {
|
|
|
147
197
|
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"] }) })] }));
|
|
148
198
|
}
|
|
149
199
|
function relativeTime(iso) {
|
|
200
|
+
if (!iso)
|
|
201
|
+
return "";
|
|
150
202
|
const d = new Date(iso);
|
|
151
203
|
const diff = Date.now() - d.getTime();
|
|
152
204
|
const sec = Math.floor(diff / 1000);
|
|
@@ -163,3 +215,11 @@ function relativeTime(iso) {
|
|
|
163
215
|
return `${day} dag${day === 1 ? "" : "en"} geleden`;
|
|
164
216
|
return d.toLocaleDateString("nl-NL");
|
|
165
217
|
}
|
|
218
|
+
function formatEuro(n) {
|
|
219
|
+
try {
|
|
220
|
+
return new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(n);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return `€ ${Math.round(n)}`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type LocalDiscountRow } from "../../lib/local-discounts.functions";
|
|
2
|
+
type Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
customerId: string;
|
|
6
|
+
editing?: LocalDiscountRow | null;
|
|
7
|
+
};
|
|
8
|
+
export declare function LocalDiscountFormDialog({ open, onOpenChange, customerId, editing }: Props): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=LocalDiscountFormDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalDiscountFormDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LocalDiscountFormDialog.tsx"],"names":[],"mappings":"AAQA,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,iCAAiC,CAAC;AAEzC,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,KAAK,+BAiMzF"}
|