@flowselections/floriday-klanten-module 1.0.12 → 1.0.14

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 (55) hide show
  1. package/dist-lib/components/klanten/DiscountsTab.d.ts +4 -0
  2. package/dist-lib/components/klanten/DiscountsTab.d.ts.map +1 -0
  3. package/dist-lib/components/klanten/DiscountsTab.js +92 -0
  4. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
  5. package/dist-lib/components/klanten/GroupedKlantDetail.js +4 -1
  6. package/dist-lib/components/klanten/ImportKlanten.d.ts +2 -0
  7. package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -0
  8. package/dist-lib/components/klanten/ImportKlanten.js +406 -0
  9. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
  10. package/dist-lib/components/klanten/KlantDetail.js +2 -1
  11. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  12. package/dist-lib/components/klanten/KlantenOverzicht.js +83 -23
  13. package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts +10 -0
  14. package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts.map +1 -0
  15. package/dist-lib/components/klanten/LocalDiscountFormDialog.js +107 -0
  16. package/dist-lib/components/klanten/LocalDiscountsTab.d.ts +4 -0
  17. package/dist-lib/components/klanten/LocalDiscountsTab.d.ts.map +1 -0
  18. package/dist-lib/components/klanten/LocalDiscountsTab.js +101 -0
  19. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
  20. package/dist-lib/components/klanten/NieuweKlant.js +10 -1
  21. package/dist-lib/index.d.ts +1 -0
  22. package/dist-lib/index.d.ts.map +1 -1
  23. package/dist-lib/index.js +1 -0
  24. package/dist-lib/integrations/supabase/auth-middleware.d.ts +1857 -32
  25. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/client.d.ts +1857 -32
  27. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  28. package/dist-lib/integrations/supabase/client.server.d.ts +1857 -32
  29. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  30. package/dist-lib/integrations/supabase/types.d.ts +1873 -18
  31. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  32. package/dist-lib/lib/accounting.functions.d.ts +5790 -315
  33. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  34. package/dist-lib/lib/customer-analytics.functions.d.ts +1857 -32
  35. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/customer-schemas.d.ts +147 -0
  37. package/dist-lib/lib/customer-schemas.d.ts.map +1 -1
  38. package/dist-lib/lib/customer-schemas.js +64 -7
  39. package/dist-lib/lib/customers.functions.d.ts +24512 -182
  40. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  41. package/dist-lib/lib/customers.functions.js +76 -17
  42. package/dist-lib/lib/delivery-locations.functions.d.ts +7300 -0
  43. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/external-customers.functions.d.ts +7300 -0
  45. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/floriday-discounts.functions.d.ts +22716 -0
  47. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -0
  48. package/dist-lib/lib/floriday-discounts.functions.js +83 -0
  49. package/dist-lib/lib/local-discounts.functions.d.ts +79495 -0
  50. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -0
  51. package/dist-lib/lib/local-discounts.functions.js +185 -0
  52. package/dist-lib/lib/overdue-invoices.functions.d.ts +11355 -0
  53. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -0
  54. package/dist-lib/lib/overdue-invoices.functions.js +72 -0
  55. package/package.json +9 -2
@@ -0,0 +1,4 @@
1
+ export declare function DiscountsTab({ buyerOrganizationId }: {
2
+ buyerOrganizationId: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=DiscountsTab.d.ts.map
@@ -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":"AAoBA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA4MpE"}
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"]);
@@ -46,7 +49,7 @@ export function GroupedKlantDetail({ groupKey }) {
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 = [];
@@ -0,0 +1,2 @@
1
+ export declare function ImportKlanten(): import("react").JSX.Element;
2
+ //# sourceMappingURL=ImportKlanten.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"AAgMA,wBAAgB,aAAa,gCAuZ5B"}
@@ -0,0 +1,406 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { useServerFn } from "@tanstack/react-start";
6
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, toast, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge, Checkbox, } from "@flowselections/core";
7
+ import { ArrowLeft, Upload, Download, FileText, CheckCircle2, AlertTriangle, XCircle } from "lucide-react";
8
+ import * as XLSX from "xlsx";
9
+ import { bulkImportCustomers } from "../../lib/customers.functions";
10
+ import { customerImportRowSchema } from "../../lib/customer-schemas";
11
+ const REQUIRED = ["company_name", "email", "street", "house_number", "country"];
12
+ // Alle interne doelvelden die de importer ondersteunt (voor handmatige mapping)
13
+ const TARGET_FIELDS = [
14
+ { key: "company_name", label: "Bedrijfsnaam", required: true },
15
+ { key: "email", label: "E-mailadres", required: true },
16
+ { key: "street", label: "Straat", required: true },
17
+ { key: "house_number", label: "Huisnummer", required: true },
18
+ { key: "country", label: "Land", required: true },
19
+ { key: "customer_number", label: "Klantnummer" },
20
+ { key: "language", label: "Taal" },
21
+ { key: "website", label: "Website" },
22
+ { key: "billing_email", label: "E-mailadres voor facturering" },
23
+ { key: "address_type", label: "Adrestype" },
24
+ { key: "address_line1", label: "Adresregel 1" },
25
+ { key: "address_line2", label: "Adresregel 2" },
26
+ { key: "postal_code", label: "Postcode" },
27
+ { key: "city", label: "Plaats" },
28
+ { key: "icp_country_code", label: "ICP landcode" },
29
+ { key: "attention_to", label: "T.a.v." },
30
+ { key: "phone", label: "Telefoonnummer" },
31
+ { key: "fax", label: "Faxnummer" },
32
+ { key: "vat_number", label: "Btw-nummer" },
33
+ { key: "coc_number", label: "KvK-nummer" },
34
+ { key: "contact_person", label: "Contactpersoon" },
35
+ { key: "customer_class", label: "Klantklasse" },
36
+ { key: "notes", label: "Notities" },
37
+ { key: "is_active", label: "Actief" },
38
+ { key: "bank_name", label: "Bank - Naam" },
39
+ { key: "bank_account_holder", label: "Bank - Rekeninghouder" },
40
+ { key: "bank_account_number", label: "Bank - Rekeningnummer" },
41
+ { key: "bank_iban", label: "Bank - IBAN" },
42
+ { key: "bank_bic", label: "Bank - BIC" },
43
+ { key: "bank_national_code", label: "Bank - Nationale bankcode" },
44
+ { key: "bank_address_line1", label: "Bank - Adresregel 1" },
45
+ { key: "bank_address_line2", label: "Bank - Adresregel 2" },
46
+ { key: "bank_house_number", label: "Bank - Huisnummer" },
47
+ { key: "bank_postal_code", label: "Bank - Postcode" },
48
+ { key: "bank_city", label: "Bank - Plaats" },
49
+ { key: "bank_state", label: "Bank - Staat" },
50
+ { key: "bank_country", label: "Bank - Land" },
51
+ { key: "discount_percentage", label: "Kortingspercentage" },
52
+ { key: "discount_days", label: "Aantal kortingsdagen" },
53
+ { key: "payment_term_days", label: "Aantal vervaldagen" },
54
+ { key: "vat_display", label: "Btw-weergave op factuur" },
55
+ ];
56
+ // Mapping van herkende (Nederlandse of Engelse) CSV-kolomnamen -> intern veld.
57
+ // Alles wordt case-insensitive vergeleken.
58
+ const HEADER_ALIASES = {
59
+ // basis
60
+ "nummer": "customer_number", "customer_number": "customer_number",
61
+ "naam": "company_name", "bedrijfsnaam": "company_name", "company_name": "company_name",
62
+ "taal": "language", "language": "language",
63
+ "website": "website",
64
+ "e-mailadres voor facturering": "billing_email", "billing_email": "billing_email",
65
+ "e-mailadres": "email", "email": "email", "e-mail": "email",
66
+ "adrestype": "address_type", "address_type": "address_type",
67
+ "adresregel 1": "address_line1", "adresregel1": "address_line1", "address_line1": "address_line1",
68
+ "adresregel 2": "address_line2", "adresregel2": "address_line2", "address_line2": "address_line2",
69
+ "straat": "street", "street": "street",
70
+ "huisnummer": "house_number", "house_number": "house_number",
71
+ "postcode": "postal_code", "postal_code": "postal_code",
72
+ "plaats": "city", "stad": "city", "city": "city",
73
+ "land": "country", "country": "country",
74
+ "icp land code": "icp_country_code", "icp landcode": "icp_country_code", "icp_country_code": "icp_country_code",
75
+ "t.a.v.": "attention_to", "tav": "attention_to", "attention_to": "attention_to",
76
+ "telefoonnummer": "phone", "telefoon": "phone", "phone": "phone",
77
+ "faxnummer": "fax", "fax": "fax",
78
+ "btw-nummer": "vat_number", "btw nummer": "vat_number", "vat_number": "vat_number",
79
+ "kvk-nummer": "coc_number", "kvk nummer": "coc_number", "coc_number": "coc_number",
80
+ "contactpersoon": "contact_person", "contact_person": "contact_person",
81
+ "klantklasse": "customer_class", "customer_class": "customer_class",
82
+ "notities": "notes", "notes": "notes",
83
+ // bank
84
+ "bank - naam": "bank_name", "bank naam": "bank_name", "bank_name": "bank_name",
85
+ "bank - rekeninghouder": "bank_account_holder", "bank_account_holder": "bank_account_holder",
86
+ "bank - rekeningnummer": "bank_account_number", "bank_account_number": "bank_account_number",
87
+ "bank - iban": "bank_iban", "bank iban": "bank_iban", "bank_iban": "bank_iban", "iban": "bank_iban",
88
+ "bank - bic": "bank_bic", "bic": "bank_bic", "bank_bic": "bank_bic",
89
+ "bank - nationale bankcode": "bank_national_code", "bank_national_code": "bank_national_code",
90
+ "bank - adresregel 1": "bank_address_line1", "bank_address_line1": "bank_address_line1",
91
+ "bank - adresregel 2": "bank_address_line2", "bank_address_line2": "bank_address_line2",
92
+ "bank - huisnummer": "bank_house_number", "bank_house_number": "bank_house_number",
93
+ "bank - postcode": "bank_postal_code", "bank_postal_code": "bank_postal_code",
94
+ "bank - plaats": "bank_city", "bank_city": "bank_city",
95
+ "bank - staat": "bank_state", "bank_state": "bank_state",
96
+ "bank - land": "bank_country", "bank_country": "bank_country",
97
+ // facturering
98
+ "kortingspercentage": "discount_percentage", "discount_percentage": "discount_percentage",
99
+ "aantal kortingsdagen": "discount_days", "discount_days": "discount_days",
100
+ "aantal vervaldagen": "payment_term_days", "payment_term_days": "payment_term_days",
101
+ "btw-weergave op factuur": "vat_display", "btw weergave op factuur": "vat_display", "vat_display": "vat_display",
102
+ // status
103
+ "actief": "is_active", "is_active": "is_active",
104
+ };
105
+ const TEMPLATE_HEADERS = [
106
+ "Nummer", "Naam", "Taal", "Website", "E-mailadres voor facturering",
107
+ "Adrestype", "Adresregel 1", "Adresregel 2", "Postcode", "Plaats", "Land",
108
+ "ICP land code", "T.a.v.", "Telefoonnummer", "Faxnummer",
109
+ "Btw-nummer", "KvK-nummer", "E-mailadres",
110
+ "Bank - Naam", "Bank - Rekeninghouder", "Bank - Rekeningnummer",
111
+ "Bank - IBAN", "Bank - BIC", "Bank - Nationale bankcode",
112
+ "Bank - Adresregel 1", "Bank - Adresregel 2", "Bank - Huisnummer",
113
+ "Bank - Postcode", "Bank - Plaats", "Bank - Staat", "Bank - Land",
114
+ "Kortingspercentage", "Aantal kortingsdagen", "Aantal vervaldagen",
115
+ "Btw-weergave op factuur",
116
+ "Straat", "Huisnummer", "Contactpersoon", "Klantklasse", "Notities", "Actief",
117
+ ];
118
+ const TEMPLATE_CSV = TEMPLATE_HEADERS.map((h) => `"${h}"`).join(",") + "\n" +
119
+ `"K-001","Voorbeeld B.V.","nl","https://voorbeeld.nl","facturen@voorbeeld.nl","Bezoek","Hoofdstraat 12","","1234AB","Amsterdam","Nederland","NL","Jan Jansen","+31612345678","","NL123456789B01","12345678","info@voorbeeld.nl","ING","Voorbeeld B.V.","","NL91INGB0002445588","INGBNL2A","","Adres 1","","1","1000AA","Amsterdam","","Nederland","2","14","30","Exclusief","Hoofdstraat","12","Jan Jansen","Groothandel","","true"\n`;
120
+ function parseCSV(text) {
121
+ const rows = [];
122
+ let cur = [];
123
+ let field = "";
124
+ let inQuotes = false;
125
+ for (let i = 0; i < text.length; i++) {
126
+ const ch = text[i];
127
+ if (inQuotes) {
128
+ if (ch === '"') {
129
+ if (text[i + 1] === '"') {
130
+ field += '"';
131
+ i++;
132
+ }
133
+ else {
134
+ inQuotes = false;
135
+ }
136
+ }
137
+ else
138
+ field += ch;
139
+ }
140
+ else {
141
+ if (ch === '"')
142
+ inQuotes = true;
143
+ else if (ch === "," || ch === ";" || ch === "\t") {
144
+ cur.push(field);
145
+ field = "";
146
+ }
147
+ else if (ch === "\n" || ch === "\r") {
148
+ if (field !== "" || cur.length > 0) {
149
+ cur.push(field);
150
+ rows.push(cur);
151
+ cur = [];
152
+ field = "";
153
+ }
154
+ if (ch === "\r" && text[i + 1] === "\n")
155
+ i++;
156
+ }
157
+ else
158
+ field += ch;
159
+ }
160
+ }
161
+ if (field !== "" || cur.length > 0) {
162
+ cur.push(field);
163
+ rows.push(cur);
164
+ }
165
+ if (rows.length === 0)
166
+ return { headers: [], rows: [] };
167
+ const headers = rows[0].map((h) => h.trim());
168
+ const dataRows = rows.slice(1)
169
+ .filter((r) => r.some((c) => c.trim() !== ""))
170
+ .map((r) => {
171
+ const obj = {};
172
+ headers.forEach((h, idx) => { obj[h] = (r[idx] ?? "").trim(); });
173
+ return obj;
174
+ });
175
+ return { headers, rows: dataRows };
176
+ }
177
+ function toBool(v) {
178
+ if (v === undefined || v === "")
179
+ return true;
180
+ return !["false", "0", "nee", "no"].includes(v.toLowerCase());
181
+ }
182
+ function rowsFromXlsx(buf) {
183
+ const wb = XLSX.read(buf, { type: "array" });
184
+ const sheet = wb.Sheets[wb.SheetNames[0]];
185
+ if (!sheet)
186
+ return { headers: [], rows: [] };
187
+ const matrix = XLSX.utils.sheet_to_json(sheet, {
188
+ header: 1, raw: false, defval: "",
189
+ });
190
+ if (matrix.length === 0)
191
+ return { headers: [], rows: [] };
192
+ const headers = matrix[0].map((h) => String(h ?? "").trim());
193
+ const dataRows = matrix.slice(1)
194
+ .filter((r) => r.some((c) => String(c ?? "").trim() !== ""))
195
+ .map((r) => {
196
+ const obj = {};
197
+ headers.forEach((h, idx) => { obj[h] = String(r[idx] ?? "").trim(); });
198
+ return obj;
199
+ });
200
+ return { headers, rows: dataRows };
201
+ }
202
+ export function ImportKlanten() {
203
+ const navigate = useNavigate();
204
+ const importFn = useServerFn(bulkImportCustomers);
205
+ const queryClient = useQueryClient();
206
+ const [sourceHeaders, setSourceHeaders] = useState([]);
207
+ const [sourceRows, setSourceRows] = useState([]);
208
+ const [mapping, setMapping] = useState({});
209
+ const [rows, setRows] = useState([]);
210
+ const [fileName, setFileName] = useState("");
211
+ const [skipDuplicates, setSkipDuplicates] = useState(true);
212
+ const [result, setResult] = useState(null);
213
+ const mut = useMutation({
214
+ mutationFn: (validRows) => importFn({ data: { rows: validRows, skipDuplicates } }),
215
+ onSuccess: (r) => {
216
+ setResult(r);
217
+ if (r.created > 0)
218
+ toast.success(`${r.created} klant${r.created === 1 ? "" : "en"} geïmporteerd`);
219
+ if (r.failed > 0)
220
+ toast.error(`${r.failed} rij${r.failed === 1 ? "" : "en"} mislukt`);
221
+ if (r.created > 0) {
222
+ queryClient.invalidateQueries({ queryKey: ["customers-local"] });
223
+ queryClient.invalidateQueries({ queryKey: ["customers-external"] });
224
+ }
225
+ },
226
+ onError: (e) => toast.error(e instanceof Error ? e.message : "Import mislukt"),
227
+ });
228
+ const validCount = useMemo(() => rows.filter((r) => r.data && r.errors.length === 0).length, [rows]);
229
+ const invalidCount = rows.length - validCount;
230
+ const handleFile = async (file) => {
231
+ setResult(null);
232
+ setRows([]);
233
+ setFileName(file.name);
234
+ const isXlsx = /\.xlsx?$/i.test(file.name)
235
+ || file.type.includes("spreadsheetml")
236
+ || file.type.includes("ms-excel");
237
+ const { headers, rows: raw } = isXlsx
238
+ ? rowsFromXlsx(await file.arrayBuffer())
239
+ : parseCSV(await file.text());
240
+ if (raw.length === 0) {
241
+ toast.error("Geen rijen gevonden in bestand");
242
+ setSourceHeaders([]);
243
+ setSourceRows([]);
244
+ setMapping({});
245
+ return;
246
+ }
247
+ // Auto-map: probeer elke bronkolom via HEADER_ALIASES te matchen aan een intern veld.
248
+ const auto = {};
249
+ const used = new Set();
250
+ for (const h of headers) {
251
+ const key = h.trim().toLowerCase();
252
+ const target = HEADER_ALIASES[key];
253
+ if (target && !used.has(target)) {
254
+ auto[h] = target;
255
+ used.add(target);
256
+ }
257
+ else {
258
+ auto[h] = "";
259
+ }
260
+ }
261
+ setSourceHeaders(headers);
262
+ setSourceRows(raw);
263
+ setMapping(auto);
264
+ };
265
+ const buildParsed = (srcRows, map) => {
266
+ return srcRows.map((sr) => {
267
+ // Herbouw rij op basis van huidige mapping
268
+ const r = {};
269
+ for (const [srcHeader, target] of Object.entries(map)) {
270
+ if (!target)
271
+ continue;
272
+ const val = sr[srcHeader];
273
+ if (val !== undefined && val !== "")
274
+ r[target] = val;
275
+ }
276
+ // Fallback: als street/house_number ontbreken maar adresregel 1 is
277
+ // gevuld, gebruik dan die als straat (verplicht veld).
278
+ const street = r.street || r.address_line1 || "";
279
+ const houseNumber = r.house_number || "-";
280
+ const candidate = {
281
+ company_name: r.company_name ?? "",
282
+ email: r.email ?? "",
283
+ street,
284
+ house_number: houseNumber,
285
+ country: r.country ?? "",
286
+ vat_number: r.vat_number ?? "",
287
+ contact_person: r.contact_person ?? "",
288
+ phone: r.phone ?? "",
289
+ postal_code: r.postal_code ?? "",
290
+ city: r.city ?? "",
291
+ customer_class: r.customer_class ?? "",
292
+ notes: r.notes ?? "",
293
+ is_active: toBool(r.is_active),
294
+ customer_number: r.customer_number ?? "",
295
+ language: r.language ?? "",
296
+ website: r.website ?? "",
297
+ billing_email: r.billing_email ?? "",
298
+ address_type: r.address_type ?? "",
299
+ address_line1: r.address_line1 ?? "",
300
+ address_line2: r.address_line2 ?? "",
301
+ icp_country_code: r.icp_country_code ?? "",
302
+ attention_to: r.attention_to ?? "",
303
+ fax: r.fax ?? "",
304
+ coc_number: r.coc_number ?? "",
305
+ bank_name: r.bank_name ?? "",
306
+ bank_account_holder: r.bank_account_holder ?? "",
307
+ bank_account_number: r.bank_account_number ?? "",
308
+ bank_iban: r.bank_iban ?? "",
309
+ bank_bic: r.bank_bic ?? "",
310
+ bank_national_code: r.bank_national_code ?? "",
311
+ bank_address_line1: r.bank_address_line1 ?? "",
312
+ bank_address_line2: r.bank_address_line2 ?? "",
313
+ bank_house_number: r.bank_house_number ?? "",
314
+ bank_postal_code: r.bank_postal_code ?? "",
315
+ bank_city: r.bank_city ?? "",
316
+ bank_state: r.bank_state ?? "",
317
+ bank_country: r.bank_country ?? "",
318
+ discount_percentage: r.discount_percentage ?? "",
319
+ discount_days: r.discount_days ?? "",
320
+ payment_term_days: r.payment_term_days ?? "",
321
+ vat_display: r.vat_display ?? "",
322
+ };
323
+ const res = customerImportRowSchema.safeParse(candidate);
324
+ if (res.success)
325
+ return { raw: r, data: res.data, errors: [] };
326
+ return {
327
+ raw: r,
328
+ data: null,
329
+ errors: res.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`),
330
+ };
331
+ });
332
+ };
333
+ // Preview automatisch bijwerken wanneer bron of mapping verandert.
334
+ useEffect(() => {
335
+ if (sourceRows.length === 0) {
336
+ setRows([]);
337
+ return;
338
+ }
339
+ setRows(buildParsed(sourceRows, mapping));
340
+ // eslint-disable-next-line react-hooks/exhaustive-deps
341
+ }, [sourceRows, mapping]);
342
+ const downloadTemplateCsv = () => {
343
+ const blob = new Blob([TEMPLATE_CSV], { type: "text/csv;charset=utf-8" });
344
+ const url = URL.createObjectURL(blob);
345
+ const a = document.createElement("a");
346
+ a.href = url;
347
+ a.download = "klanten-import-template.csv";
348
+ a.click();
349
+ URL.revokeObjectURL(url);
350
+ };
351
+ const downloadTemplateXlsx = () => {
352
+ const sampleRow = {
353
+ "Nummer": "K-001", "Naam": "Voorbeeld B.V.", "Taal": "nl",
354
+ "Website": "https://voorbeeld.nl",
355
+ "E-mailadres voor facturering": "facturen@voorbeeld.nl",
356
+ "Adrestype": "Bezoek", "Adresregel 1": "Hoofdstraat 12", "Adresregel 2": "",
357
+ "Postcode": "1234AB", "Plaats": "Amsterdam", "Land": "Nederland",
358
+ "ICP land code": "NL", "T.a.v.": "Jan Jansen",
359
+ "Telefoonnummer": "+31612345678", "Faxnummer": "",
360
+ "Btw-nummer": "NL123456789B01", "KvK-nummer": "12345678",
361
+ "E-mailadres": "info@voorbeeld.nl",
362
+ "Bank - Naam": "ING", "Bank - Rekeninghouder": "Voorbeeld B.V.",
363
+ "Bank - Rekeningnummer": "", "Bank - IBAN": "NL91INGB0002445588",
364
+ "Bank - BIC": "INGBNL2A", "Bank - Nationale bankcode": "",
365
+ "Bank - Adresregel 1": "Adres 1", "Bank - Adresregel 2": "",
366
+ "Bank - Huisnummer": "1", "Bank - Postcode": "1000AA",
367
+ "Bank - Plaats": "Amsterdam", "Bank - Staat": "", "Bank - Land": "Nederland",
368
+ "Kortingspercentage": "2", "Aantal kortingsdagen": "14",
369
+ "Aantal vervaldagen": "30", "Btw-weergave op factuur": "Exclusief",
370
+ "Straat": "Hoofdstraat", "Huisnummer": "12",
371
+ "Contactpersoon": "Jan Jansen", "Klantklasse": "Groothandel",
372
+ "Notities": "", "Actief": "true",
373
+ };
374
+ const ws = XLSX.utils.json_to_sheet([sampleRow], { header: TEMPLATE_HEADERS });
375
+ const wb = XLSX.utils.book_new();
376
+ XLSX.utils.book_append_sheet(wb, ws, "Klanten");
377
+ XLSX.writeFile(wb, "klanten-import-template.xlsx");
378
+ };
379
+ const runImport = () => {
380
+ const valid = rows.filter((r) => r.data && r.errors.length === 0).map((r) => r.data);
381
+ if (valid.length === 0) {
382
+ toast.error("Geen geldige rijen om te importeren");
383
+ return;
384
+ }
385
+ mut.mutate(valid);
386
+ };
387
+ return (_jsxs("main", { className: "p-6 max-w-5xl mx-auto space-y-4", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug"] }), _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 importeren" }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: ["Upload een CSV om meerdere klanten tegelijk toe te voegen. Verplichte kolommen: ", REQUIRED.join(", "), "."] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: downloadTemplateXlsx, children: [_jsx(Download, { className: "h-4 w-4 mr-2" }), " Template (.xlsx)"] }), _jsxs(Button, { variant: "outline", onClick: downloadTemplateCsv, children: [_jsx(Download, { className: "h-4 w-4 mr-2" }), " Template (.csv)"] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "1. Bestand kiezen" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Label, { htmlFor: "csvfile", className: "sr-only", children: "Excel- of CSV-bestand" }), _jsx(Input, { id: "csvfile", type: "file", accept: ".csv,.xlsx,.xls,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel", onChange: (e) => { const f = e.target.files?.[0]; if (f)
388
+ handleFile(f); }, className: "max-w-sm" }), fileName && (_jsxs("span", { className: "inline-flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(FileText, { className: "h-4 w-4" }), " ", fileName] }))] }), _jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { checked: skipDuplicates, onCheckedChange: (v) => setSkipDuplicates(v === true) }), "Duplicaten overslaan (op e-mail, BTW of vergelijkbare bedrijfsnaam)"] })] })] }), sourceHeaders.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base", children: ["2. Velden koppelen (", sourceHeaders.length, " kolommen)"] }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Kies per bronkolom naar welk klantveld deze moet worden ge\u00EFmporteerd." })] }), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[420px] overflow-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Bronkolom (uit bestand)" }), _jsx(TableHead, { children: "Voorbeeldwaarde" }), _jsx(TableHead, { className: "w-[280px]", children: "Koppelen aan klantveld" })] }) }), _jsx(TableBody, { children: sourceHeaders.map((h) => {
389
+ const sample = sourceRows.find((r) => r[h])?.[h] ?? "";
390
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "text-sm font-medium", children: h || _jsx("span", { className: "text-muted-foreground italic", children: "(zonder naam)" }) }), _jsx(TableCell, { className: "text-xs text-muted-foreground truncate max-w-[280px]", children: sample }), _jsx(TableCell, { children: _jsxs("select", { className: "w-full h-9 rounded-md border border-input bg-background px-2 text-sm", value: mapping[h] ?? "", onChange: (e) => setMapping((m) => ({ ...m, [h]: e.target.value })), children: [_jsx("option", { value: "", children: "\u2014 Negeren \u2014" }), TARGET_FIELDS.map((f) => (_jsxs("option", { value: f.key, children: [f.label, f.required ? " *" : ""] }, f.key)))] }) })] }, h));
391
+ }) })] }) }) }), _jsxs("div", { className: "flex items-center justify-between gap-2 p-4 border-t", children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Verplicht: ", TARGET_FIELDS.filter((f) => f.required).map((f) => f.label).join(", ")] }), _jsx(Button, { variant: "outline", onClick: () => {
392
+ setSourceHeaders([]);
393
+ setSourceRows([]);
394
+ setMapping({});
395
+ setRows([]);
396
+ setFileName("");
397
+ setResult(null);
398
+ }, children: "Wissen" })] })] })), rows.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base", children: ["3. Voorbeeld (", rows.length, " rijen)"] }), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsxs(Badge, { variant: "secondary", className: "bg-success/10 text-success", children: [_jsx(CheckCircle2, { className: "h-3 w-3 mr-1" }), " ", validCount, " geldig"] }), invalidCount > 0 && (_jsxs(Badge, { variant: "secondary", className: "bg-destructive/10 text-destructive", children: [_jsx(XCircle, { className: "h-3 w-3 mr-1" }), " ", invalidCount, " ongeldig"] }))] })] }), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[420px] overflow-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-10", children: "#" }), _jsx(TableHead, { children: "Bedrijf" }), _jsx(TableHead, { children: "E-mail" }), _jsx(TableHead, { children: "Plaats" }), _jsx(TableHead, { children: "Land" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: rows.map((r, i) => (_jsxs(TableRow, { className: r.errors.length > 0 ? "bg-destructive/5" : "", children: [_jsx(TableCell, { className: "text-xs text-muted-foreground", children: i + 1 }), _jsx(TableCell, { className: "text-sm", children: r.raw.company_name || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-sm", children: r.raw.email || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-sm", children: r.raw.city || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-sm", children: r.raw.country || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { children: r.errors.length === 0 ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-success", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), " OK"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-destructive", title: r.errors.join("\n"), children: [_jsx(AlertTriangle, { className: "h-3.5 w-3.5" }), " ", r.errors[0]] })) })] }, i))) })] }) }) }), _jsxs("div", { className: "flex justify-end gap-2 p-4 border-t", children: [_jsx(Button, { variant: "outline", onClick: () => {
399
+ setRows([]);
400
+ setFileName("");
401
+ setResult(null);
402
+ setSourceHeaders([]);
403
+ setSourceRows([]);
404
+ setMapping({});
405
+ }, children: "Wissen" }), _jsxs(Button, { onClick: runImport, disabled: mut.isPending || validCount === 0, children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), mut.isPending ? "Importeren..." : `Importeer ${validCount} klant${validCount === 1 ? "" : "en"}`] })] })] })), result && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Resultaat" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsxs(Badge, { className: "bg-success/10 text-success", children: [result.created, " aangemaakt"] }), _jsxs(Badge, { className: "bg-warning/10 text-warning", children: [result.skipped, " overgeslagen"] }), _jsxs(Badge, { className: "bg-destructive/10 text-destructive", children: [result.failed, " mislukt"] })] }), result.errors.length > 0 && (_jsx("div", { className: "max-h-64 overflow-auto border rounded-md", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-10", children: "#" }), _jsx(TableHead, { children: "Bedrijf" }), _jsx(TableHead, { children: "Reden" })] }) }), _jsx(TableBody, { children: result.errors.map((e, i) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "text-xs", children: e.row }), _jsx(TableCell, { className: "text-sm", children: e.company_name }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: e.reason })] }, i))) })] }) })), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: () => navigate({ to: "/klanten" }), children: "Naar klantenlijst" }) })] })] }))] }));
406
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAkPjD"}
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"}