@flowselections/floriday-klanten-module 1.0.29 → 1.0.30

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 (66) hide show
  1. package/dist-lib/components/klanten/BewerkKlant.d.ts +6 -1
  2. package/dist-lib/components/klanten/BewerkKlant.d.ts.map +1 -1
  3. package/dist-lib/components/klanten/BewerkKlant.js +7 -4
  4. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
  5. package/dist-lib/components/klanten/GroupedKlantDetail.js +4 -2
  6. package/dist-lib/components/klanten/KlantBewerkenDialog.d.ts +12 -0
  7. package/dist-lib/components/klanten/KlantBewerkenDialog.d.ts.map +1 -0
  8. package/dist-lib/components/klanten/KlantBewerkenDialog.js +27 -0
  9. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
  10. package/dist-lib/components/klanten/KlantDetail.js +5 -3
  11. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  12. package/dist-lib/components/klanten/KlantenOverzicht.js +65 -11
  13. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts +7 -1
  14. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts.map +1 -1
  15. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.js +14 -7
  16. package/dist-lib/index.d.ts +1 -0
  17. package/dist-lib/index.d.ts.map +1 -1
  18. package/dist-lib/index.js +1 -0
  19. package/dist-lib/integrations/supabase/auth-middleware.d.ts +490 -0
  20. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/client.d.ts +490 -0
  22. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  23. package/dist-lib/integrations/supabase/client.server.d.ts +490 -0
  24. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/types.d.ts +490 -0
  26. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  27. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1470 -0
  28. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/accounting.functions.d.ts +1470 -0
  30. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/customer-analytics.functions.d.ts +490 -0
  32. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/customer-notes.functions.d.ts +980 -0
  34. package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
  35. package/dist-lib/lib/customer-revenue.functions.d.ts +20327 -0
  36. package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -0
  37. package/dist-lib/lib/customer-revenue.functions.js +124 -0
  38. package/dist-lib/lib/customers.functions.d.ts +4410 -0
  39. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/delivery-locations.functions.d.ts +1960 -0
  41. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  42. package/dist-lib/lib/external-customers.functions.d.ts +1960 -0
  43. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/floriday-assortment.functions.d.ts +490 -0
  45. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/floriday-discounts.functions.d.ts +980 -0
  47. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  48. package/dist-lib/lib/floriday-trade-items.functions.d.ts +490 -0
  49. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/local-discounts.functions.d.ts +3430 -0
  51. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  52. package/dist-lib/lib/logistics-events.functions.d.ts +490 -0
  53. package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
  54. package/dist-lib/lib/logistics-strategy.functions.d.ts +1960 -0
  55. package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
  56. package/dist-lib/lib/overdue-invoices.functions.d.ts +490 -0
  57. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  58. package/dist-lib/lib/price-groups-local.functions.d.ts +3430 -0
  59. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
  60. package/dist-lib/lib/price-groups.functions.d.ts +490 -0
  61. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
  62. package/dist-lib/lib/twinfield-adopt.functions.d.ts +980 -0
  63. package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
  64. package/dist-lib/lib/twinfield-ledger.functions.d.ts +490 -0
  65. package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
  66. package/package.json +2 -2
@@ -1,4 +1,9 @@
1
- export declare function BewerkKlant({ id }: {
1
+ export declare function BewerkKlant({ id, onDone, onCancel, hideBackLink, }: {
2
2
  id: string;
3
+ /** Wordt aangeroepen na succesvol opslaan; navigeert dan zelf niet. */
4
+ onDone?: () => void;
5
+ /** Wordt aangeroepen bij annuleren; navigeert dan zelf niet. */
6
+ onCancel?: () => void;
7
+ hideBackLink?: boolean;
3
8
  }): import("react").JSX.Element;
4
9
  //# sourceMappingURL=BewerkKlant.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAmDjD"}
1
+ {"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,MAAM,EACN,QAAQ,EACR,YAAY,GACb,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,+BAsDA"}
@@ -27,7 +27,7 @@ function toFormValues(c) {
27
27
  out.force = true; // bij bewerken nooit de duplicaatcontrole
28
28
  return out;
29
29
  }
30
- export function BewerkKlant({ id }) {
30
+ export function BewerkKlant({ id, onDone, onCancel, hideBackLink, }) {
31
31
  const navigate = useNavigate();
32
32
  const qc = useQueryClient();
33
33
  const get = useServerFn(getCustomer);
@@ -46,15 +46,18 @@ export function BewerkKlant({ id }) {
46
46
  qc.invalidateQueries({ queryKey: ["customer", id] });
47
47
  qc.invalidateQueries({ queryKey: ["customers"] });
48
48
  qc.invalidateQueries({ queryKey: ["twinfield-sync-status", id] });
49
- navigate({ to: "/klanten/$id", params: { id } });
49
+ if (onDone)
50
+ onDone();
51
+ else
52
+ navigate({ to: "/klanten/$id", params: { id } });
50
53
  },
51
54
  onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
52
55
  });
53
56
  if (isLoading)
54
57
  return _jsx("main", { className: "p-6", children: "Laden..." });
55
58
  if (error || !data?.customer) {
56
- return (_jsxs("main", { className: "p-6 space-y-3", children: [_jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Klant niet gevonden." })] }));
59
+ return (_jsxs("main", { className: "p-6 space-y-3", children: [!hideBackLink && _jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Klant niet gevonden." })] }));
57
60
  }
58
61
  const c = data.customer;
59
- return (_jsx(KlantFormulier, { title: `${c.company_name || "Klant"} bewerken`, description: "Pas de gegevens aan. Wijzigingen worden direct naar Twinfield doorgestuurd.", submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: toFormValues(c), onSubmit: (values) => mut.mutate(values), onCancel: () => navigate({ to: "/klanten/$id", params: { id } }) }));
62
+ return (_jsx(KlantFormulier, { title: `${c.company_name || "Klant"} bewerken`, description: "Pas de gegevens aan. Wijzigingen worden direct naar Twinfield doorgestuurd.", submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: toFormValues(c), onSubmit: (values) => mut.mutate(values), onCancel: () => onCancel ? onCancel() : navigate({ to: "/klanten/$id", params: { id } }) }));
60
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA4TpE"}
1
+ {"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BAgUpE"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useNavigate, Link } from "@tanstack/react-router";
2
+ import { useNavigate } from "@tanstack/react-router";
3
3
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { useEffect, useState } from "react";
@@ -17,6 +17,7 @@ import { FloridayAssortmentTab } from "../../components/klanten/FloridayAssortme
17
17
  import { decodeDeep } from "../../lib/decode-html";
18
18
  import { LogisticsStrategyCard } from "../../components/klanten/LogisticsStrategyCard";
19
19
  import { DeliveryLocationsTab } from "../../components/klanten/DeliveryLocationsTab";
20
+ import { KlantBewerkenDialog } from "../../components/klanten/KlantBewerkenDialog";
20
21
  import { getCustomerNote, upsertCustomerNote } from "../../lib/customer-notes.functions";
21
22
  import { supabase } from "../../integrations/supabase/client";
22
23
  export function GroupedKlantDetail({ groupKey }) {
@@ -27,6 +28,7 @@ export function GroupedKlantDetail({ groupKey }) {
27
28
  const getNote = useServerFn(getCustomerNote);
28
29
  const saveNote = useServerFn(upsertCustomerNote);
29
30
  const [tab, setTab] = useState("overview");
31
+ const [editOpen, setEditOpen] = useState(false);
30
32
  const { data, isLoading, error } = useQuery({
31
33
  queryKey: ["grouped-customer", groupKey],
32
34
  queryFn: () => get({ data: { groupKey } }).then(decodeDeep),
@@ -89,7 +91,7 @@ export function GroupedKlantDetail({ groupKey }) {
89
91
  provider: providerMap.get(s.provider)?.label ?? s.provider,
90
92
  }))
91
93
  .sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
92
- return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id: localCustomerId }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) })) : twinfieldCode ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/overnemen/$code", params: { code: twinfieldCode }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
94
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : twinfieldCode ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: localCustomerId ?? null, twinfieldCode: localCustomerId ? null : twinfieldCode, onSaved: () => qc.invalidateQueries() }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
93
95
  contacts: contacts.length,
94
96
  locations: locations.length,
95
97
  assortment: 0,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bewerken/overnemen in een dialoog, zodat de module geen eigen routes nodig
3
+ * heeft. Werkt daardoor in elke shell, ongeacht welke routes die declareert.
4
+ */
5
+ export declare function KlantBewerkenDialog({ open, onOpenChange, customerId, twinfieldCode, onSaved, }: {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ customerId?: string | null;
9
+ twinfieldCode?: string | null;
10
+ onSaved?: (customerId: string | null) => void;
11
+ }): import("react").JSX.Element;
12
+ //# sourceMappingURL=KlantBewerkenDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KlantBewerkenDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantBewerkenDialog.tsx"],"names":[],"mappings":"AAOA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,aAAa,EACb,OAAO,GACR,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC/C,+BA0CA"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@flowselections/core";
4
+ import { BewerkKlant } from "./BewerkKlant";
5
+ import { OvernemenTwinfieldKlant } from "./OvernemenTwinfieldKlant";
6
+ /**
7
+ * Bewerken/overnemen in een dialoog, zodat de module geen eigen routes nodig
8
+ * heeft. Werkt daardoor in elke shell, ongeacht welke routes die declareert.
9
+ */
10
+ export function KlantBewerkenDialog({ open, onOpenChange, customerId, twinfieldCode, onSaved, }) {
11
+ // Bij overnemen kan blijken dat de klant lokaal al bestaat; dan tonen we
12
+ // meteen het bewerkformulier binnen dezelfde dialoog.
13
+ const [adoptedId, setAdoptedId] = useState(null);
14
+ useEffect(() => {
15
+ if (!open)
16
+ setAdoptedId(null);
17
+ }, [open]);
18
+ const editId = customerId ?? adoptedId;
19
+ const close = () => onOpenChange(false);
20
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-5xl max-h-[90vh] overflow-y-auto p-0", children: [_jsx(DialogHeader, { className: "sr-only", children: _jsx(DialogTitle, { children: "Klant bewerken" }) }), editId ? (_jsx(BewerkKlant, { id: editId, hideBackLink: true, onDone: () => {
21
+ onSaved?.(editId);
22
+ close();
23
+ }, onCancel: close })) : twinfieldCode ? (_jsx(OvernemenTwinfieldKlant, { code: twinfieldCode, hideBackLink: true, onExistingLocal: setAdoptedId, onDone: (id) => {
24
+ onSaved?.(id);
25
+ close();
26
+ }, onCancel: close })) : null] }) }));
27
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAsTjD"}
1
+ {"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BA2TjD"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Link, useNavigate } from "@tanstack/react-router";
2
+ import { useNavigate } from "@tanstack/react-router";
3
3
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { useEffect, useState } from "react";
@@ -20,6 +20,7 @@ import { decodeDeep } from "../../lib/decode-html";
20
20
  import { getMissingTwinfieldFields } from "../../lib/twinfield-required-fields";
21
21
  import { getCustomerNote, upsertCustomerNote } from "../../lib/customer-notes.functions";
22
22
  import { supabase } from "../../integrations/supabase/client";
23
+ import { KlantBewerkenDialog } from "../../components/klanten/KlantBewerkenDialog";
23
24
  export function KlantDetail({ id }) {
24
25
  const navigate = useNavigate();
25
26
  const qc = useQueryClient();
@@ -29,6 +30,7 @@ export function KlantDetail({ id }) {
29
30
  const getNote = useServerFn(getCustomerNote);
30
31
  const saveNote = useServerFn(upsertCustomerNote);
31
32
  const [tab, setTab] = useState("overview");
33
+ const [editOpen, setEditOpen] = useState(false);
32
34
  const { data, isLoading } = useQuery({
33
35
  queryKey: ["customer", id],
34
36
  queryFn: () => get({ data: { id } }).then(decodeDeep),
@@ -70,10 +72,10 @@ export function KlantDetail({ id }) {
70
72
  const c = data.customer;
71
73
  const missingTwinfield = getMissingTwinfieldFields(c);
72
74
  const priceGroups = data.price_groups ?? c.price_groups ?? [];
73
- return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) }), _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), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
75
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }), _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), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
74
76
  contacts: data.contacts.length,
75
77
  locations: data.delivery_locations.length,
76
78
  assortment: data.assortment.length,
77
79
  orders: data.orders.length,
78
- }, 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, ")"] }), c.floriday_organization_id && (_jsx(TabsTrigger, { value: "floriday-assortment", children: "Floriday-aanbod" })), _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: [_jsx(LogisticsStrategyCard, { scope: "customer", refKey: id }), 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.bio && !(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))) })] }) }) })) }), c.floriday_organization_id && (_jsx(TabsContent, { value: "floriday-assortment", children: _jsx(FloridayAssortmentTab, { organizationId: c.floriday_organization_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 }) })] })] })] })] }));
80
+ }, 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, ")"] }), c.floriday_organization_id && (_jsx(TabsTrigger, { value: "floriday-assortment", children: "Floriday-aanbod" })), _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: [_jsx(LogisticsStrategyCard, { scope: "customer", refKey: id }), 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.bio && !(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))) })] }) }) })) }), c.floriday_organization_id && (_jsx(TabsContent, { value: "floriday-assortment", children: _jsx(FloridayAssortmentTab, { organizationId: c.floriday_organization_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 }) })] })] })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: id, onSaved: () => qc.invalidateQueries({ queryKey: ["customer", id] }) })] }));
79
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,gBAAgB,gCAkrB/B"}
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,gBAAgB,gCAkvB/B"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Link, useNavigate } from "@tanstack/react-router";
2
+ import { useNavigate } from "@tanstack/react-router";
3
3
  import { useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { useEffect, useMemo, useRef, useState } from "react";
@@ -10,10 +10,12 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { listCustomers, listCustomerClasses } from "../../lib/customers.functions";
11
11
  import { listExternalCustomers } from "../../lib/external-customers.functions";
12
12
  import { listOverdueBalances } from "../../lib/overdue-invoices.functions";
13
+ import { listRevenueActivity } from "../../lib/customer-revenue.functions";
13
14
  import { supabase } from "../../integrations/supabase/client";
14
15
  import { decodeDeep } from "../../lib/decode-html";
15
16
  import { CustomerAvatar } from "../CustomerAvatar";
16
17
  import { PriceGroupBadges } from "./PriceGroupBadges";
18
+ import { KlantBewerkenDialog } from "./KlantBewerkenDialog";
17
19
  import { ChannelBadges } from "./ChannelBadges";
18
20
  import { derivePaymentMethod, paymentToneClasses } from "../../lib/payment-method";
19
21
  import { formatCountry, getCountryFlagAndName, countryNameToFlag } from "../../lib/countries";
@@ -29,6 +31,7 @@ export function KlantenOverzicht() {
29
31
  const [view, setView] = usePersistedState("klanten:view", "all");
30
32
  const [sort, setSort] = usePersistedState("klanten:sort", { col: "company_name", dir: "asc" });
31
33
  const [page, setPage] = usePersistedState("klanten:page", 1);
34
+ const [editTarget, setEditTarget] = useState(null);
32
35
  const [pageSize, setPageSize] = usePersistedState("klanten:pageSize", 25, "local");
33
36
  // Zoekveld: de invoer reageert direct, het filteren pas na een korte pauze,
34
37
  // zodat typen soepel blijft ook bij een volledig klantenbestand.
@@ -57,6 +60,7 @@ export function KlantenOverzicht() {
57
60
  const listExternal = useServerFn(listExternalCustomers);
58
61
  const classesFn = useServerFn(listCustomerClasses);
59
62
  const overdueFn = useServerFn(listOverdueBalances);
63
+ const revenueFn = useServerFn(listRevenueActivity);
60
64
  const { data: classData } = useQuery({
61
65
  queryKey: ["customer-classes"],
62
66
  queryFn: () => classesFn({ data: undefined }).then(decodeDeep),
@@ -79,6 +83,11 @@ export function KlantenOverzicht() {
79
83
  queryFn: () => overdueFn().then(decodeDeep),
80
84
  staleTime: 60000,
81
85
  });
86
+ const { data: revenueData, isLoading: loadingRevenue } = useQuery({
87
+ queryKey: ["customer-revenue-12m"],
88
+ queryFn: () => revenueFn().then(decodeDeep),
89
+ staleTime: 60000,
90
+ });
82
91
  // Realtime: refetch de klantenlijst zodra de Twinfield/Floriday cache verandert.
83
92
  // De DB-trigger heeft de materialized view al ververst, dus een simpele
84
93
  // invalidatie levert direct verse data. Debounced om bulk-syncs op te vangen.
@@ -90,6 +99,7 @@ export function KlantenOverzicht() {
90
99
  refreshTimer.current = setTimeout(() => {
91
100
  queryClient.invalidateQueries({ queryKey: ["customers-external"] });
92
101
  queryClient.invalidateQueries({ queryKey: ["overdue-balances"] });
102
+ queryClient.invalidateQueries({ queryKey: ["customer-revenue-12m"] });
93
103
  }, 750);
94
104
  };
95
105
  const channel = supabase
@@ -108,6 +118,26 @@ export function KlantenOverzicht() {
108
118
  const allRows = useMemo(() => {
109
119
  const tw = overdueData?.by_provider?.twinfield ?? {};
110
120
  const ex = overdueData?.by_provider?.exact ?? {};
121
+ const rev = revenueData?.by_provider;
122
+ const revFor = (provider, externalId) => {
123
+ const map = provider === "twinfield" ? rev?.twinfield
124
+ : provider === "exact" ? rev?.exact
125
+ : provider === "floriday" ? rev?.floriday
126
+ : provider === "local" ? rev?.local
127
+ : undefined;
128
+ return map?.[externalId];
129
+ };
130
+ const mergeRevenue = (into, hit) => {
131
+ if (!hit)
132
+ return into;
133
+ if (!into)
134
+ return { ...hit };
135
+ return {
136
+ count: into.count + hit.count,
137
+ amount: into.amount + hit.amount,
138
+ last_date: hit.last_date > into.last_date ? hit.last_date : into.last_date,
139
+ };
140
+ };
111
141
  // Verzamel lokale customer-id's die in de externe grouped view meemerged zijn
112
142
  // met een andere provider (bijv. Twinfield/Floriday via BTW-nummer). Voor die
113
143
  // klanten laten we de lokale rij vallen en gebruiken we de merged rij.
@@ -144,6 +174,7 @@ export function KlantenOverzicht() {
144
174
  logo_url: c.logo_url ?? null,
145
175
  is_active: !!c.is_active,
146
176
  overdue: null,
177
+ revenue: mergeRevenue(mergeRevenue(null, revFor("local", String(c.id))), c.twinfield_code ? revFor("twinfield", String(c.twinfield_code)) : undefined),
147
178
  price_groups: c.price_groups ?? [],
148
179
  payment_method: derivePaymentMethod(null, c),
149
180
  twinfield_code: c.twinfield_code ?? null,
@@ -160,6 +191,10 @@ export function KlantenOverzicht() {
160
191
  const twinfieldSource = (c.sources ?? [])
161
192
  .find((s) => s.provider === "twinfield");
162
193
  let overdue = null;
194
+ let revenue = null;
195
+ for (const s of (c.sources ?? [])) {
196
+ revenue = mergeRevenue(revenue, revFor(s.provider, s.external_id));
197
+ }
163
198
  for (const s of (c.sources ?? [])) {
164
199
  const hit = s.provider === "twinfield" ? tw[s.external_id]
165
200
  : s.provider === "exact" ? ex[s.external_id]
@@ -189,22 +224,24 @@ export function KlantenOverzicht() {
189
224
  logo_url: c.logo_url,
190
225
  is_active: c.is_active,
191
226
  overdue,
227
+ revenue,
192
228
  price_groups: localRow?.price_groups ?? [],
193
229
  payment_method: derivePaymentMethod((c.sources ?? []), localRow),
194
230
  twinfield_code: twinfieldSource?.external_id ?? localRow?.twinfield_code ?? null,
195
231
  };
196
232
  });
197
233
  return [...local, ...ext];
198
- }, [localData, externalData, overdueData]);
234
+ }, [localData, externalData, overdueData, revenueData]);
199
235
  const totals = useMemo(() => {
200
236
  const t = {
201
237
  all: allRows.length,
202
- active: allRows.filter((c) => c.is_active).length,
203
- inactive: allRows.filter((c) => !c.is_active).length,
238
+ // Actief = minimaal één factuur/order in de afgelopen 12 maanden.
239
+ active: loadingRevenue ? null : allRows.filter((c) => (c.revenue?.count ?? 0) > 0).length,
240
+ inactive: loadingRevenue ? null : allRows.filter((c) => (c.revenue?.count ?? 0) === 0).length,
204
241
  overdue: allRows.filter((c) => (c.overdue?.count ?? 0) > 0).length,
205
242
  };
206
243
  return t;
207
- }, [allRows]);
244
+ }, [allRows, loadingRevenue]);
208
245
  const countries = useMemo(() => {
209
246
  const set = new Set();
210
247
  allRows.forEach((c) => {
@@ -247,9 +284,9 @@ export function KlantenOverzicht() {
247
284
  const customers = useMemo(() => {
248
285
  let rows = allRows;
249
286
  if (view === "active")
250
- rows = rows.filter((c) => c.is_active);
287
+ rows = rows.filter((c) => (c.revenue?.count ?? 0) > 0);
251
288
  if (view === "inactive")
252
- rows = rows.filter((c) => !c.is_active);
289
+ rows = rows.filter((c) => (c.revenue?.count ?? 0) === 0);
253
290
  if (view === "overdue")
254
291
  rows = rows.filter((c) => (c.overdue?.count ?? 0) > 0);
255
292
  if (selectedClasses.length > 0)
@@ -328,7 +365,11 @@ export function KlantenOverzicht() {
328
365
  });
329
366
  const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
330
367
  const lastUpdated = externalData?.last_fetched_at ?? null;
331
- 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: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ 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?.totals
368
+ 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: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ 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: revenueData
369
+ ? `Klanten met minimaal één bestelling of factuur sinds ${formatDateNl(revenueData.since)}.\nTotale omzet in die periode: ${formatEuro(revenueData.totals.amount)} · ${revenueData.totals.transactions} transacties`
370
+ : "Klanten met minimaal één 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: revenueData
371
+ ? `Klanten zonder enige bestelling of factuur sinds ${formatDateNl(revenueData.since)}.`
372
+ : "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?.totals
332
373
  ? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen`
333
374
  : "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, { "aria-label": "Zoek klanten", placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: searchInput, onChange: (e) => setSearchInput(e.target.value), className: "pl-9" })] }), _jsx(MultiSearchableSelect, { values: selectedClasses, onValuesChange: (v) => { setSelectedClasses(v); resetPage(); }, options: classOptions, placeholder: "Alle klassen", triggerClassName: "w-40" }), _jsx(MultiSearchableSelect, { values: selectedCountries, onValuesChange: (v) => { setSelectedCountries(v); resetPage(); }, options: countryOptions, placeholder: "Alle landen", triggerClassName: "w-36" }), _jsx(MultiSearchableSelect, { values: selectedChannels, onValuesChange: (v) => { setSelectedChannels(v); resetPage(); }, options: channelSelectOptions, placeholder: "Alle kanalen", triggerClassName: "w-40" }), _jsx(MultiSearchableSelect, { values: selectedPayments, onValuesChange: (v) => { setSelectedPayments(v); resetPage(); }, options: paymentSelectOptions, placeholder: "Alle betaalvormen", triggerClassName: "w-40" })] }), 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: () => {
334
375
  setSearchInput("");
@@ -339,7 +380,7 @@ export function KlantenOverzicht() {
339
380
  setSelectedPayments([]);
340
381
  setView("all");
341
382
  setPage(1);
342
- }, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", 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: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _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: 8 }).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: 8, children: _jsx(EmptyState, { onAdd: () => goTo({ to: "/klanten/nieuw" }), filtered: activeFilterChips.length > 0, onClearFilters: () => {
383
+ }, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", 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: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _jsx(TableHead, { children: "Omzet 12 mnd" }), _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: 8 }).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: 9, children: _jsx(EmptyState, { onAdd: () => goTo({ to: "/klanten/nieuw" }), filtered: activeFilterChips.length > 0, onClearFilters: () => {
343
384
  setSearchInput("");
344
385
  setSearch("");
345
386
  setSelectedClasses([]);
@@ -357,8 +398,15 @@ export function KlantenOverzicht() {
357
398
  : goTo({
358
399
  to: "/klanten/groep/$groupKey",
359
400
  params: { groupKey: c.group_key },
360
- }), 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", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: formatCountry(c.country) })] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(StatusPill, { active: c.is_active }), c.id ? (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Klant bewerken", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id: c.id }, onClick: (e) => { e.stopPropagation(); markReturnToList(); }, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] }) })) : c.twinfield_code ? (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Twinfield-klant overnemen en bewerken", children: _jsxs(Link, { to: "/klanten/overnemen/$code", params: { code: c.twinfield_code }, onClick: (e) => { e.stopPropagation(); markReturnToList(); }, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] }) })) : null] }) })] }, c.key));
361
- }) })] }) }) }), _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" })] })] })] })] })] }) }));
401
+ }), 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", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm whitespace-nowrap", children: c.revenue && c.revenue.count > 0 ? (_jsxs("span", { title: `${c.revenue.count} transactie${c.revenue.count === 1 ? "" : "s"} · laatste op ${formatDateNl(c.revenue.last_date)}`, children: [_jsx("span", { className: "font-medium", children: formatEuro(c.revenue.amount) }), _jsxs("span", { className: "block text-xs text-muted-foreground", children: ["laatst ", formatDateNl(c.revenue.last_date)] })] })) : (_jsx("span", { className: "text-muted-foreground", children: "Geen omzet" })) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: formatCountry(c.country) })] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(StatusPill, { active: c.is_active }), c.id ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Klant bewerken", onClick: (e) => {
402
+ e.stopPropagation();
403
+ setEditTarget({ customerId: c.id });
404
+ }, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] })) : c.twinfield_code ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Twinfield-klant overnemen en bewerken", onClick: (e) => {
405
+ e.stopPropagation();
406
+ setEditTarget({ twinfieldCode: c.twinfield_code });
407
+ }, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] })) : null] }) })] }, c.key));
408
+ }) })] }) }) }), _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" })] })] })] })] }), _jsx(KlantBewerkenDialog, { open: editTarget !== null, onOpenChange: (open) => { if (!open)
409
+ setEditTarget(null); }, customerId: editTarget?.customerId ?? null, twinfieldCode: editTarget?.twinfieldCode ?? null, onSaved: () => queryClient.invalidateQueries() })] }) }));
362
410
  }
363
411
  function StatCard({ icon, label, value, active, onClick, tooltip, tint = "muted", disabled = false, }) {
364
412
  const tintCls = {
@@ -406,6 +454,12 @@ function relativeTime(iso) {
406
454
  return `${day} dag${day === 1 ? "" : "en"} geleden`;
407
455
  return d.toLocaleDateString("nl-NL");
408
456
  }
457
+ function formatDateNl(iso) {
458
+ const d = new Date(`${iso}T00:00:00`);
459
+ if (Number.isNaN(d.getTime()))
460
+ return iso;
461
+ return d.toLocaleDateString("nl-NL", { day: "numeric", month: "short", year: "numeric" });
462
+ }
409
463
  function formatEuro(n) {
410
464
  try {
411
465
  return new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(n);
@@ -2,7 +2,13 @@
2
2
  * Neemt een Twinfield-debiteur over naar een lokale, bewerkbare klant. Bestaat
3
3
  * de lokale klant al, dan sturen we meteen door naar het bewerkscherm.
4
4
  */
5
- export declare function OvernemenTwinfieldKlant({ code }: {
5
+ export declare function OvernemenTwinfieldKlant({ code, onExistingLocal, onDone, onCancel, hideBackLink, }: {
6
6
  code: string;
7
+ /** Bestaat er al een lokale klant, dan geeft dit het id door i.p.v. te navigeren. */
8
+ onExistingLocal?: (id: string) => void;
9
+ /** Na succesvol overnemen; navigeert dan zelf niet. */
10
+ onDone?: (customerId: string | null) => void;
11
+ onCancel?: () => void;
12
+ hideBackLink?: boolean;
7
13
  }): import("react").JSX.Element;
8
14
  //# sourceMappingURL=OvernemenTwinfieldKlant.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"OvernemenTwinfieldKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/OvernemenTwinfieldKlant.tsx"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,+BAkGjE"}
1
+ {"version":3,"file":"OvernemenTwinfieldKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/OvernemenTwinfieldKlant.tsx"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,eAAe,EACf,MAAM,EACN,QAAQ,EACR,YAAY,GACb,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,uDAAuD;IACvD,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,+BAsGA"}
@@ -21,7 +21,7 @@ function num(v) {
21
21
  * Neemt een Twinfield-debiteur over naar een lokale, bewerkbare klant. Bestaat
22
22
  * de lokale klant al, dan sturen we meteen door naar het bewerkscherm.
23
23
  */
24
- export function OvernemenTwinfieldKlant({ code }) {
24
+ export function OvernemenTwinfieldKlant({ code, onExistingLocal, onDone, onCancel, hideBackLink, }) {
25
25
  const navigate = useNavigate();
26
26
  const qc = useQueryClient();
27
27
  const prepare = useServerFn(prepareTwinfieldAdoption);
@@ -32,9 +32,13 @@ export function OvernemenTwinfieldKlant({ code }) {
32
32
  });
33
33
  const existingId = data?.localId ?? null;
34
34
  useEffect(() => {
35
- if (existingId)
35
+ if (!existingId)
36
+ return;
37
+ if (onExistingLocal)
38
+ onExistingLocal(existingId);
39
+ else
36
40
  navigate({ to: "/klanten/bewerken/$id", params: { id: existingId }, replace: true });
37
- }, [existingId, navigate]);
41
+ }, [existingId, navigate, onExistingLocal]);
38
42
  const mut = useMutation({
39
43
  mutationFn: (values) => adopt({ data: { code, values } }),
40
44
  onSuccess: (r) => {
@@ -44,8 +48,11 @@ export function OvernemenTwinfieldKlant({ code }) {
44
48
  }
45
49
  qc.invalidateQueries({ queryKey: ["customers-local"] });
46
50
  qc.invalidateQueries({ queryKey: ["customers-external"] });
47
- const id = r.customer?.id;
48
- if (id)
51
+ qc.invalidateQueries({ queryKey: ["customers"] });
52
+ const id = r.customer?.id ?? null;
53
+ if (onDone)
54
+ onDone(id);
55
+ else if (id)
49
56
  navigate({ to: "/klanten/$id", params: { id } });
50
57
  else
51
58
  navigate({ to: "/klanten" });
@@ -55,7 +62,7 @@ export function OvernemenTwinfieldKlant({ code }) {
55
62
  if (isLoading || existingId)
56
63
  return _jsx("main", { className: "p-6", children: "Laden..." });
57
64
  if (error || !data?.prefill) {
58
- return (_jsxs("main", { className: "p-6 space-y-3", children: [_jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Deze Twinfield-klant is niet gevonden." })] }));
65
+ return (_jsxs("main", { className: "p-6 space-y-3", children: [!hideBackLink && _jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Deze Twinfield-klant is niet gevonden." })] }));
59
66
  }
60
67
  const p = data.prefill;
61
68
  const defaults = {
@@ -96,5 +103,5 @@ export function OvernemenTwinfieldKlant({ code }) {
96
103
  customer_number: code,
97
104
  force: true,
98
105
  };
99
- return (_jsx(KlantFormulier, { title: `${p.company_name || `Debiteur ${code}`} bewerken`, submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: defaults, numberReadOnly: true, numberHint: `Twinfield-debiteurnummer ${code} — niet aanpasbaar`, onSubmit: (values) => mut.mutate(values), onCancel: () => navigate({ to: "/klanten" }) }));
106
+ return (_jsx(KlantFormulier, { title: `${p.company_name || `Debiteur ${code}`} bewerken`, submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: defaults, numberReadOnly: true, numberHint: `Twinfield-debiteurnummer ${code} — niet aanpasbaar`, onSubmit: (values) => mut.mutate(values), onCancel: () => (onCancel ? onCancel() : navigate({ to: "/klanten" })) }));
100
107
  }
@@ -8,6 +8,7 @@ export { NieuweKlant } from "./components/klanten/NieuweKlant";
8
8
  export { KlantFormulier } from "./components/klanten/KlantFormulier";
9
9
  export { BewerkKlant } from "./components/klanten/BewerkKlant";
10
10
  export { BewerkKlantRoute } from "./components/klanten/BewerkKlantRoute";
11
+ export { KlantBewerkenDialog } from "./components/klanten/KlantBewerkenDialog";
11
12
  export { OvernemenTwinfieldKlant } from "./components/klanten/OvernemenTwinfieldKlant";
12
13
  export { OvernemenTwinfieldKlantRoute } from "./components/klanten/OvernemenTwinfieldKlantRoute";
13
14
  export { ImportKlanten } from "./components/klanten/ImportKlanten";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,UAgBnC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,UAgBnC,CAAC"}
package/dist-lib/index.js CHANGED
@@ -24,6 +24,7 @@ export { NieuweKlant } from "./components/klanten/NieuweKlant";
24
24
  export { KlantFormulier } from "./components/klanten/KlantFormulier";
25
25
  export { BewerkKlant } from "./components/klanten/BewerkKlant";
26
26
  export { BewerkKlantRoute } from "./components/klanten/BewerkKlantRoute";
27
+ export { KlantBewerkenDialog } from "./components/klanten/KlantBewerkenDialog";
27
28
  export { OvernemenTwinfieldKlant } from "./components/klanten/OvernemenTwinfieldKlant";
28
29
  export { OvernemenTwinfieldKlantRoute } from "./components/klanten/OvernemenTwinfieldKlantRoute";
29
30
  export { ImportKlanten } from "./components/klanten/ImportKlanten";