@flowselections/floriday-klanten-module 1.0.28 → 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 (86) 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 +9 -5
  4. package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts.map +1 -1
  5. package/dist-lib/components/klanten/DeliveryLocationsTab.js +2 -1
  6. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
  7. package/dist-lib/components/klanten/GroupedKlantDetail.js +4 -2
  8. package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -1
  9. package/dist-lib/components/klanten/ImportKlanten.js +2 -1
  10. package/dist-lib/components/klanten/KlantBewerkenDialog.d.ts +12 -0
  11. package/dist-lib/components/klanten/KlantBewerkenDialog.d.ts.map +1 -0
  12. package/dist-lib/components/klanten/KlantBewerkenDialog.js +27 -0
  13. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
  14. package/dist-lib/components/klanten/KlantDetail.js +5 -3
  15. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  16. package/dist-lib/components/klanten/KlantenOverzicht.js +102 -16
  17. package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts.map +1 -1
  18. package/dist-lib/components/klanten/LocalDiscountFormDialog.js +2 -1
  19. package/dist-lib/components/klanten/LocalDiscountsTab.d.ts.map +1 -1
  20. package/dist-lib/components/klanten/LocalDiscountsTab.js +2 -1
  21. package/dist-lib/components/klanten/LogisticsStrategyCard.d.ts.map +1 -1
  22. package/dist-lib/components/klanten/LogisticsStrategyCard.js +2 -1
  23. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
  24. package/dist-lib/components/klanten/NieuweKlant.js +2 -1
  25. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts +7 -1
  26. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts.map +1 -1
  27. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.js +16 -8
  28. package/dist-lib/components/klanten/RouteError.d.ts +6 -0
  29. package/dist-lib/components/klanten/RouteError.d.ts.map +1 -0
  30. package/dist-lib/components/klanten/RouteError.js +13 -0
  31. package/dist-lib/index.d.ts +1 -0
  32. package/dist-lib/index.d.ts.map +1 -1
  33. package/dist-lib/index.js +1 -0
  34. package/dist-lib/integrations/supabase/auth-middleware.d.ts +490 -0
  35. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  36. package/dist-lib/integrations/supabase/client.d.ts +490 -0
  37. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  38. package/dist-lib/integrations/supabase/client.server.d.ts +490 -0
  39. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  40. package/dist-lib/integrations/supabase/types.d.ts +490 -0
  41. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  42. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1470 -0
  43. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/accounting.functions.d.ts +1470 -0
  45. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/customer-analytics.functions.d.ts +490 -0
  47. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  48. package/dist-lib/lib/customer-notes.functions.d.ts +980 -0
  49. package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/customer-revenue.functions.d.ts +20327 -0
  51. package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -0
  52. package/dist-lib/lib/customer-revenue.functions.js +124 -0
  53. package/dist-lib/lib/customers.functions.d.ts +4410 -0
  54. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/customers.functions.js +39 -35
  56. package/dist-lib/lib/delivery-locations.functions.d.ts +1960 -0
  57. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  58. package/dist-lib/lib/errors.d.ts +7 -0
  59. package/dist-lib/lib/errors.d.ts.map +1 -0
  60. package/dist-lib/lib/errors.js +36 -0
  61. package/dist-lib/lib/external-customers.functions.d.ts +1961 -1
  62. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  63. package/dist-lib/lib/external-customers.functions.js +28 -12
  64. package/dist-lib/lib/floriday-assortment.functions.d.ts +490 -0
  65. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
  66. package/dist-lib/lib/floriday-discounts.functions.d.ts +980 -0
  67. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  68. package/dist-lib/lib/floriday-trade-items.functions.d.ts +490 -0
  69. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
  70. package/dist-lib/lib/local-discounts.functions.d.ts +3430 -0
  71. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  72. package/dist-lib/lib/logistics-events.functions.d.ts +490 -0
  73. package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
  74. package/dist-lib/lib/logistics-strategy.functions.d.ts +1960 -0
  75. package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
  76. package/dist-lib/lib/overdue-invoices.functions.d.ts +490 -0
  77. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  78. package/dist-lib/lib/price-groups-local.functions.d.ts +3430 -0
  79. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
  80. package/dist-lib/lib/price-groups.functions.d.ts +490 -0
  81. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
  82. package/dist-lib/lib/twinfield-adopt.functions.d.ts +980 -0
  83. package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
  84. package/dist-lib/lib/twinfield-ledger.functions.d.ts +490 -0
  85. package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
  86. 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":"AA4BA,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"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { friendlyError } from "../../lib/errors";
2
3
  import { useNavigate } from "@tanstack/react-router";
3
4
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
5
  import { useServerFn } from "@tanstack/react-start";
@@ -26,7 +27,7 @@ function toFormValues(c) {
26
27
  out.force = true; // bij bewerken nooit de duplicaatcontrole
27
28
  return out;
28
29
  }
29
- export function BewerkKlant({ id }) {
30
+ export function BewerkKlant({ id, onDone, onCancel, hideBackLink, }) {
30
31
  const navigate = useNavigate();
31
32
  const qc = useQueryClient();
32
33
  const get = useServerFn(getCustomer);
@@ -45,15 +46,18 @@ export function BewerkKlant({ id }) {
45
46
  qc.invalidateQueries({ queryKey: ["customer", id] });
46
47
  qc.invalidateQueries({ queryKey: ["customers"] });
47
48
  qc.invalidateQueries({ queryKey: ["twinfield-sync-status", id] });
48
- navigate({ to: "/klanten/$id", params: { id } });
49
+ if (onDone)
50
+ onDone();
51
+ else
52
+ navigate({ to: "/klanten/$id", params: { id } });
49
53
  },
50
- onError: (e) => toast.error(e instanceof Error ? e.message : "Opslaan mislukt"),
54
+ onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
51
55
  });
52
56
  if (isLoading)
53
57
  return _jsx("main", { className: "p-6", children: "Laden..." });
54
58
  if (error || !data?.customer) {
55
- 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." })] }));
56
60
  }
57
61
  const c = data.customer;
58
- 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 } }) }));
59
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DeliveryLocationsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/DeliveryLocationsTab.tsx"],"names":[],"mappings":"AAmEA,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,+BAoaA"}
1
+ {"version":3,"file":"DeliveryLocationsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/DeliveryLocationsTab.tsx"],"names":[],"mappings":"AAoEA,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,+BAoaA"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { friendlyError } from "../../lib/errors";
2
3
  import { useEffect, useMemo, useRef, useState } from "react";
3
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
5
  import { useServerFn } from "@tanstack/react-start";
@@ -118,7 +119,7 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
118
119
  qc.invalidateQueries({ queryKey: ["customer", ownerKey] });
119
120
  setOpen(false);
120
121
  },
121
- onError: (e) => toast.error(e?.message ?? "Opslaan mislukt"),
122
+ onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
122
123
  });
123
124
  const delMut = useMutation({
124
125
  mutationFn: (id) => deleteFn({ data: { id } }),
@@ -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,
@@ -1 +1 @@
1
- {"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"AA8MA,wBAAgB,aAAa,gCAsc5B"}
1
+ {"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"AA+MA,wBAAgB,aAAa,gCAsc5B"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { friendlyError } from "../../lib/errors";
2
3
  import { useNavigate } from "@tanstack/react-router";
3
4
  import { useEffect, useMemo, useState } from "react";
4
5
  import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -235,7 +236,7 @@ export function ImportKlanten() {
235
236
  queryClient.invalidateQueries({ queryKey: ["customers-external"] });
236
237
  }
237
238
  },
238
- onError: (e) => toast.error(e instanceof Error ? e.message : "Import mislukt"),
239
+ onError: (e) => toast.error(friendlyError(e, "Import mislukt")),
239
240
  });
240
241
  const validCount = useMemo(() => rows.filter((r) => r.data && r.errors.length === 0).length, [rows]);
241
242
  const invalidCount = rows.length - validCount;
@@ -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,gCA+oB/B"}
1
+ {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,gBAAgB,gCAkvB/B"}
@@ -1,8 +1,8 @@
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
- import { useEffect, useMemo, useRef } from "react";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
6
  import { usePersistedState, markReturnToList, consumeReturnToList } from "../../lib/persisted-state";
7
7
  import { Button, Input, Card, CardContent, Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Skeleton, } from "@flowselections/core";
8
8
  import { Plus, Search, Users, UserCheck, UserX, AlertCircle, X, ArrowUpDown, Mail, MapPin, Upload, ChevronLeft, ChevronRight, Pencil, } from "lucide-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,7 +31,21 @@ 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");
36
+ // Zoekveld: de invoer reageert direct, het filteren pas na een korte pauze,
37
+ // zodat typen soepel blijft ook bij een volledig klantenbestand.
38
+ const [searchInput, setSearchInput] = useState(search);
39
+ useEffect(() => {
40
+ if (searchInput === search)
41
+ return;
42
+ const t = setTimeout(() => {
43
+ setSearch(searchInput);
44
+ setPage(1);
45
+ }, 250);
46
+ return () => clearTimeout(t);
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [searchInput]);
33
49
  // Paginanummer alleen bewaren binnen de klantenflow; vers binnenkomen start op pagina 1.
34
50
  useEffect(() => {
35
51
  if (!consumeReturnToList())
@@ -44,25 +60,34 @@ export function KlantenOverzicht() {
44
60
  const listExternal = useServerFn(listExternalCustomers);
45
61
  const classesFn = useServerFn(listCustomerClasses);
46
62
  const overdueFn = useServerFn(listOverdueBalances);
63
+ const revenueFn = useServerFn(listRevenueActivity);
47
64
  const { data: classData } = useQuery({
48
65
  queryKey: ["customer-classes"],
49
66
  queryFn: () => classesFn({ data: undefined }).then(decodeDeep),
67
+ staleTime: 5 * 60000,
50
68
  });
51
69
  const { data: localData, isLoading: loadingLocal } = useQuery({
52
70
  queryKey: ["customers-local"],
53
71
  queryFn: () => list({
54
72
  data: { search: "", customer_class: "", country: "", is_active: "all", source: "local" },
55
73
  }).then(decodeDeep),
74
+ staleTime: 60000,
56
75
  });
57
76
  const { data: externalData, isLoading: loadingExternal } = useQuery({
58
77
  queryKey: ["customers-external"],
59
78
  queryFn: () => listExternal({ data: { status: "all" } }).then(decodeDeep),
79
+ staleTime: 60000,
60
80
  });
61
81
  const { data: overdueData } = useQuery({
62
82
  queryKey: ["overdue-balances"],
63
83
  queryFn: () => overdueFn().then(decodeDeep),
64
84
  staleTime: 60000,
65
85
  });
86
+ const { data: revenueData, isLoading: loadingRevenue } = useQuery({
87
+ queryKey: ["customer-revenue-12m"],
88
+ queryFn: () => revenueFn().then(decodeDeep),
89
+ staleTime: 60000,
90
+ });
66
91
  // Realtime: refetch de klantenlijst zodra de Twinfield/Floriday cache verandert.
67
92
  // De DB-trigger heeft de materialized view al ververst, dus een simpele
68
93
  // invalidatie levert direct verse data. Debounced om bulk-syncs op te vangen.
@@ -74,6 +99,7 @@ export function KlantenOverzicht() {
74
99
  refreshTimer.current = setTimeout(() => {
75
100
  queryClient.invalidateQueries({ queryKey: ["customers-external"] });
76
101
  queryClient.invalidateQueries({ queryKey: ["overdue-balances"] });
102
+ queryClient.invalidateQueries({ queryKey: ["customer-revenue-12m"] });
77
103
  }, 750);
78
104
  };
79
105
  const channel = supabase
@@ -92,6 +118,26 @@ export function KlantenOverzicht() {
92
118
  const allRows = useMemo(() => {
93
119
  const tw = overdueData?.by_provider?.twinfield ?? {};
94
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
+ };
95
141
  // Verzamel lokale customer-id's die in de externe grouped view meemerged zijn
96
142
  // met een andere provider (bijv. Twinfield/Floriday via BTW-nummer). Voor die
97
143
  // klanten laten we de lokale rij vallen en gebruiken we de merged rij.
@@ -128,6 +174,7 @@ export function KlantenOverzicht() {
128
174
  logo_url: c.logo_url ?? null,
129
175
  is_active: !!c.is_active,
130
176
  overdue: null,
177
+ revenue: mergeRevenue(mergeRevenue(null, revFor("local", String(c.id))), c.twinfield_code ? revFor("twinfield", String(c.twinfield_code)) : undefined),
131
178
  price_groups: c.price_groups ?? [],
132
179
  payment_method: derivePaymentMethod(null, c),
133
180
  twinfield_code: c.twinfield_code ?? null,
@@ -144,6 +191,10 @@ export function KlantenOverzicht() {
144
191
  const twinfieldSource = (c.sources ?? [])
145
192
  .find((s) => s.provider === "twinfield");
146
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
+ }
147
198
  for (const s of (c.sources ?? [])) {
148
199
  const hit = s.provider === "twinfield" ? tw[s.external_id]
149
200
  : s.provider === "exact" ? ex[s.external_id]
@@ -173,22 +224,24 @@ export function KlantenOverzicht() {
173
224
  logo_url: c.logo_url,
174
225
  is_active: c.is_active,
175
226
  overdue,
227
+ revenue,
176
228
  price_groups: localRow?.price_groups ?? [],
177
229
  payment_method: derivePaymentMethod((c.sources ?? []), localRow),
178
230
  twinfield_code: twinfieldSource?.external_id ?? localRow?.twinfield_code ?? null,
179
231
  };
180
232
  });
181
233
  return [...local, ...ext];
182
- }, [localData, externalData, overdueData]);
234
+ }, [localData, externalData, overdueData, revenueData]);
183
235
  const totals = useMemo(() => {
184
236
  const t = {
185
237
  all: allRows.length,
186
- active: allRows.filter((c) => c.is_active).length,
187
- 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,
188
241
  overdue: allRows.filter((c) => (c.overdue?.count ?? 0) > 0).length,
189
242
  };
190
243
  return t;
191
- }, [allRows]);
244
+ }, [allRows, loadingRevenue]);
192
245
  const countries = useMemo(() => {
193
246
  const set = new Set();
194
247
  allRows.forEach((c) => {
@@ -231,9 +284,9 @@ export function KlantenOverzicht() {
231
284
  const customers = useMemo(() => {
232
285
  let rows = allRows;
233
286
  if (view === "active")
234
- rows = rows.filter((c) => c.is_active);
287
+ rows = rows.filter((c) => (c.revenue?.count ?? 0) > 0);
235
288
  if (view === "inactive")
236
- rows = rows.filter((c) => !c.is_active);
289
+ rows = rows.filter((c) => (c.revenue?.count ?? 0) === 0);
237
290
  if (view === "overdue")
238
291
  rows = rows.filter((c) => (c.overdue?.count ?? 0) > 0);
239
292
  if (selectedClasses.length > 0)
@@ -275,7 +328,11 @@ export function KlantenOverzicht() {
275
328
  const resetPage = () => setPage(1);
276
329
  const activeFilterChips = [];
277
330
  if (search)
278
- activeFilterChips.push({ key: "s", label: `Zoek: "${search}"`, clear: () => { setSearch(""); resetPage(); } });
331
+ activeFilterChips.push({
332
+ key: "s",
333
+ label: `Zoek: "${search}"`,
334
+ clear: () => { setSearchInput(""); setSearch(""); resetPage(); },
335
+ });
279
336
  selectedClasses.forEach((cls) => {
280
337
  activeFilterChips.push({
281
338
  key: `c-${cls}`,
@@ -308,9 +365,14 @@ export function KlantenOverzicht() {
308
365
  });
309
366
  const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
310
367
  const lastUpdated = externalData?.last_fetched_at ?? null;
311
- 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
312
373
  ? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen`
313
- : "Klanten met openstaande facturen waarvan de vervaldatum is verstreken."), active: view === "overdue", onClick: () => { setView("overdue"); resetPage(); } })] }), _jsxs(Card, { children: [_jsx("div", { className: "border-b px-4 pt-3", children: _jsx(Tabs, { value: view, onValueChange: (v) => { setView(v); resetPage(); }, children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.all ?? 0 })] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.active ?? 0 })] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.inactive ?? 0 })] }), _jsxs(TabsTrigger, { value: "overdue", children: ["Achterstallig ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.overdue ?? 0 })] })] }) }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap p-4 border-b", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: search, onChange: (e) => { setSearch(e.target.value); resetPage(); }, className: "pl-9" })] }), _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: () => {
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: () => {
375
+ setSearchInput("");
314
376
  setSearch("");
315
377
  setSelectedClasses([]);
316
378
  setSelectedCountries([]);
@@ -318,7 +380,16 @@ export function KlantenOverzicht() {
318
380
  setSelectedPayments([]);
319
381
  setView("all");
320
382
  setPage(1);
321
- }, 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" }) }) }) })) : pageRows.map((c) => {
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: () => {
384
+ setSearchInput("");
385
+ setSearch("");
386
+ setSelectedClasses([]);
387
+ setSelectedCountries([]);
388
+ setSelectedChannels([]);
389
+ setSelectedPayments([]);
390
+ setView("all");
391
+ setPage(1);
392
+ } }) }) })) : pageRows.map((c) => {
322
393
  const secondary = c.commercial_name && c.commercial_name !== c.company_name
323
394
  ? c.commercial_name
324
395
  : c.city ?? c.email ?? null;
@@ -327,8 +398,15 @@ export function KlantenOverzicht() {
327
398
  : goTo({
328
399
  to: "/klanten/groep/$groupKey",
329
400
  params: { groupKey: c.group_key },
330
- }), 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));
331
- }) })] }) }) }), _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() })] }) }));
332
410
  }
333
411
  function StatCard({ icon, label, value, active, onClick, tooltip, tint = "muted", disabled = false, }) {
334
412
  const tintCls = {
@@ -352,8 +430,10 @@ function StatusPill({ active }) {
352
430
  ? "bg-primary/10 text-primary"
353
431
  : "bg-muted text-muted-foreground"}`, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${active ? "bg-primary" : "bg-muted-foreground"}` }), active ? "Actief" : "Inactief"] }));
354
432
  }
355
- function EmptyState({ onAdd }) {
356
- return (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-muted flex items-center justify-center", children: _jsx(Users, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Geen klanten gevonden" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Pas je filters aan of voeg handmatig een klant toe." })] }), _jsx("div", { className: "flex gap-2 mt-2", children: _jsxs(Button, { size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] }) })] }));
433
+ function EmptyState({ onAdd, filtered = false, onClearFilters, }) {
434
+ return (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-muted flex items-center justify-center", children: _jsx(Users, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: filtered ? "Geen klanten binnen deze filters" : "Nog geen klanten" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: filtered
435
+ ? "Verruim of wis je filters om meer klanten te zien."
436
+ : "Voeg je eerste klant toe of importeer een bestand." })] }), _jsxs("div", { className: "flex gap-2 mt-2", children: [filtered && onClearFilters && (_jsxs(Button, { size: "sm", variant: "outline", onClick: onClearFilters, children: [_jsx(X, { className: "h-4 w-4 mr-2" }), " Filters wissen"] })), _jsxs(Button, { size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }));
357
437
  }
358
438
  function relativeTime(iso) {
359
439
  if (!iso)
@@ -374,6 +454,12 @@ function relativeTime(iso) {
374
454
  return `${day} dag${day === 1 ? "" : "en"} geleden`;
375
455
  return d.toLocaleDateString("nl-NL");
376
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
+ }
377
463
  function formatEuro(n) {
378
464
  try {
379
465
  return new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(n);