@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.
- package/dist-lib/components/klanten/BewerkKlant.d.ts +6 -1
- package/dist-lib/components/klanten/BewerkKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/BewerkKlant.js +9 -5
- package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/DeliveryLocationsTab.js +2 -1
- package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/GroupedKlantDetail.js +4 -2
- package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -1
- package/dist-lib/components/klanten/ImportKlanten.js +2 -1
- package/dist-lib/components/klanten/KlantBewerkenDialog.d.ts +12 -0
- package/dist-lib/components/klanten/KlantBewerkenDialog.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantBewerkenDialog.js +27 -0
- package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDetail.js +5 -3
- package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenOverzicht.js +102 -16
- package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts.map +1 -1
- package/dist-lib/components/klanten/LocalDiscountFormDialog.js +2 -1
- package/dist-lib/components/klanten/LocalDiscountsTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/LocalDiscountsTab.js +2 -1
- package/dist-lib/components/klanten/LogisticsStrategyCard.d.ts.map +1 -1
- package/dist-lib/components/klanten/LogisticsStrategyCard.js +2 -1
- package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/NieuweKlant.js +2 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts +7 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.js +16 -8
- package/dist-lib/components/klanten/RouteError.d.ts +6 -0
- package/dist-lib/components/klanten/RouteError.d.ts.map +1 -0
- package/dist-lib/components/klanten/RouteError.js +13 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +490 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +490 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +490 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +490 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1470 -0
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting.functions.d.ts +1470 -0
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-analytics.functions.d.ts +490 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-notes.functions.d.ts +980 -0
- package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-revenue.functions.d.ts +20327 -0
- package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -0
- package/dist-lib/lib/customer-revenue.functions.js +124 -0
- package/dist-lib/lib/customers.functions.d.ts +4410 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/customers.functions.js +39 -35
- package/dist-lib/lib/delivery-locations.functions.d.ts +1960 -0
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/errors.d.ts +7 -0
- package/dist-lib/lib/errors.d.ts.map +1 -0
- package/dist-lib/lib/errors.js +36 -0
- package/dist-lib/lib/external-customers.functions.d.ts +1961 -1
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.js +28 -12
- package/dist-lib/lib/floriday-assortment.functions.d.ts +490 -0
- package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-discounts.functions.d.ts +980 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-trade-items.functions.d.ts +490 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
- package/dist-lib/lib/local-discounts.functions.d.ts +3430 -0
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-events.functions.d.ts +490 -0
- package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-strategy.functions.d.ts +1960 -0
- package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
- package/dist-lib/lib/overdue-invoices.functions.d.ts +490 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups-local.functions.d.ts +3430 -0
- package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups.functions.d.ts +490 -0
- package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adopt.functions.d.ts +980 -0
- package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.functions.d.ts +490 -0
- package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
- 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":"
|
|
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
|
-
|
|
49
|
+
if (onDone)
|
|
50
|
+
onDone();
|
|
51
|
+
else
|
|
52
|
+
navigate({ to: "/klanten/$id", params: { id } });
|
|
49
53
|
},
|
|
50
|
-
onError: (e) => toast.error(e
|
|
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":"
|
|
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
|
|
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":"
|
|
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
|
|
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 ? (
|
|
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":"
|
|
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
|
|
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":"
|
|
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 {
|
|
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 }),
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
187
|
-
|
|
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.
|
|
287
|
+
rows = rows.filter((c) => (c.revenue?.count ?? 0) > 0);
|
|
235
288
|
if (view === "inactive")
|
|
236
|
-
rows = rows.filter((c) =>
|
|
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({
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
331
|
-
|
|
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
|
|
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);
|