@flowselections/floriday-klanten-module 1.0.29 → 1.0.31
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 +7 -4
- 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/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 +65 -11
- 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 +14 -7
- 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 +745 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +745 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +745 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +745 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +2411 -176
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting.functions.d.ts +2411 -176
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-analytics.functions.d.ts +745 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-notes.functions.d.ts +1490 -0
- package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-revenue.functions.d.ts +20582 -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 +6705 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/delivery-locations.functions.d.ts +3332 -352
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.d.ts +3332 -352
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-assortment.functions.d.ts +745 -0
- package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-discounts.functions.d.ts +1490 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-trade-items.functions.d.ts +745 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
- package/dist-lib/lib/local-discounts.functions.d.ts +5215 -0
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-events.functions.d.ts +745 -0
- package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-strategy.functions.d.ts +3332 -352
- package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
- package/dist-lib/lib/overdue-invoices.functions.d.ts +745 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups-local.functions.d.ts +5215 -0
- package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups.functions.d.ts +745 -0
- package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adopt.functions.d.ts +1490 -0
- package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.functions.d.ts +745 -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":"AA6BA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,MAAM,EACN,QAAQ,EACR,YAAY,GACb,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,+BAsDA"}
|
|
@@ -27,7 +27,7 @@ function toFormValues(c) {
|
|
|
27
27
|
out.force = true; // bij bewerken nooit de duplicaatcontrole
|
|
28
28
|
return out;
|
|
29
29
|
}
|
|
30
|
-
export function BewerkKlant({ id }) {
|
|
30
|
+
export function BewerkKlant({ id, onDone, onCancel, hideBackLink, }) {
|
|
31
31
|
const navigate = useNavigate();
|
|
32
32
|
const qc = useQueryClient();
|
|
33
33
|
const get = useServerFn(getCustomer);
|
|
@@ -46,15 +46,18 @@ export function BewerkKlant({ id }) {
|
|
|
46
46
|
qc.invalidateQueries({ queryKey: ["customer", id] });
|
|
47
47
|
qc.invalidateQueries({ queryKey: ["customers"] });
|
|
48
48
|
qc.invalidateQueries({ queryKey: ["twinfield-sync-status", id] });
|
|
49
|
-
|
|
49
|
+
if (onDone)
|
|
50
|
+
onDone();
|
|
51
|
+
else
|
|
52
|
+
navigate({ to: "/klanten/$id", params: { id } });
|
|
50
53
|
},
|
|
51
54
|
onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
|
|
52
55
|
});
|
|
53
56
|
if (isLoading)
|
|
54
57
|
return _jsx("main", { className: "p-6", children: "Laden..." });
|
|
55
58
|
if (error || !data?.customer) {
|
|
56
|
-
return (_jsxs("main", { className: "p-6 space-y-3", children: [_jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Klant niet gevonden." })] }));
|
|
59
|
+
return (_jsxs("main", { className: "p-6 space-y-3", children: [!hideBackLink && _jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Klant niet gevonden." })] }));
|
|
57
60
|
}
|
|
58
61
|
const c = data.customer;
|
|
59
|
-
return (_jsx(KlantFormulier, { title: `${c.company_name || "Klant"} bewerken`, description: "Pas de gegevens aan. Wijzigingen worden direct naar Twinfield doorgestuurd.", submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: toFormValues(c), onSubmit: (values) => mut.mutate(values), onCancel: () => navigate({ to: "/klanten/$id", params: { id } }) }));
|
|
62
|
+
return (_jsx(KlantFormulier, { title: `${c.company_name || "Klant"} bewerken`, description: "Pas de gegevens aan. Wijzigingen worden direct naar Twinfield doorgestuurd.", submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: toFormValues(c), onSubmit: (values) => mut.mutate(values), onCancel: () => onCancel ? onCancel() : navigate({ to: "/klanten/$id", params: { id } }) }));
|
|
60
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"
|
|
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,
|
|
@@ -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,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, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
5
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -10,10 +10,12 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
|
10
10
|
import { listCustomers, listCustomerClasses } from "../../lib/customers.functions";
|
|
11
11
|
import { listExternalCustomers } from "../../lib/external-customers.functions";
|
|
12
12
|
import { listOverdueBalances } from "../../lib/overdue-invoices.functions";
|
|
13
|
+
import { listRevenueActivity } from "../../lib/customer-revenue.functions";
|
|
13
14
|
import { supabase } from "../../integrations/supabase/client";
|
|
14
15
|
import { decodeDeep } from "../../lib/decode-html";
|
|
15
16
|
import { CustomerAvatar } from "../CustomerAvatar";
|
|
16
17
|
import { PriceGroupBadges } from "./PriceGroupBadges";
|
|
18
|
+
import { KlantBewerkenDialog } from "./KlantBewerkenDialog";
|
|
17
19
|
import { ChannelBadges } from "./ChannelBadges";
|
|
18
20
|
import { derivePaymentMethod, paymentToneClasses } from "../../lib/payment-method";
|
|
19
21
|
import { formatCountry, getCountryFlagAndName, countryNameToFlag } from "../../lib/countries";
|
|
@@ -29,6 +31,7 @@ export function KlantenOverzicht() {
|
|
|
29
31
|
const [view, setView] = usePersistedState("klanten:view", "all");
|
|
30
32
|
const [sort, setSort] = usePersistedState("klanten:sort", { col: "company_name", dir: "asc" });
|
|
31
33
|
const [page, setPage] = usePersistedState("klanten:page", 1);
|
|
34
|
+
const [editTarget, setEditTarget] = useState(null);
|
|
32
35
|
const [pageSize, setPageSize] = usePersistedState("klanten:pageSize", 25, "local");
|
|
33
36
|
// Zoekveld: de invoer reageert direct, het filteren pas na een korte pauze,
|
|
34
37
|
// zodat typen soepel blijft ook bij een volledig klantenbestand.
|
|
@@ -57,6 +60,7 @@ export function KlantenOverzicht() {
|
|
|
57
60
|
const listExternal = useServerFn(listExternalCustomers);
|
|
58
61
|
const classesFn = useServerFn(listCustomerClasses);
|
|
59
62
|
const overdueFn = useServerFn(listOverdueBalances);
|
|
63
|
+
const revenueFn = useServerFn(listRevenueActivity);
|
|
60
64
|
const { data: classData } = useQuery({
|
|
61
65
|
queryKey: ["customer-classes"],
|
|
62
66
|
queryFn: () => classesFn({ data: undefined }).then(decodeDeep),
|
|
@@ -79,6 +83,11 @@ export function KlantenOverzicht() {
|
|
|
79
83
|
queryFn: () => overdueFn().then(decodeDeep),
|
|
80
84
|
staleTime: 60000,
|
|
81
85
|
});
|
|
86
|
+
const { data: revenueData, isLoading: loadingRevenue } = useQuery({
|
|
87
|
+
queryKey: ["customer-revenue-12m"],
|
|
88
|
+
queryFn: () => revenueFn().then(decodeDeep),
|
|
89
|
+
staleTime: 60000,
|
|
90
|
+
});
|
|
82
91
|
// Realtime: refetch de klantenlijst zodra de Twinfield/Floriday cache verandert.
|
|
83
92
|
// De DB-trigger heeft de materialized view al ververst, dus een simpele
|
|
84
93
|
// invalidatie levert direct verse data. Debounced om bulk-syncs op te vangen.
|
|
@@ -90,6 +99,7 @@ export function KlantenOverzicht() {
|
|
|
90
99
|
refreshTimer.current = setTimeout(() => {
|
|
91
100
|
queryClient.invalidateQueries({ queryKey: ["customers-external"] });
|
|
92
101
|
queryClient.invalidateQueries({ queryKey: ["overdue-balances"] });
|
|
102
|
+
queryClient.invalidateQueries({ queryKey: ["customer-revenue-12m"] });
|
|
93
103
|
}, 750);
|
|
94
104
|
};
|
|
95
105
|
const channel = supabase
|
|
@@ -108,6 +118,26 @@ export function KlantenOverzicht() {
|
|
|
108
118
|
const allRows = useMemo(() => {
|
|
109
119
|
const tw = overdueData?.by_provider?.twinfield ?? {};
|
|
110
120
|
const ex = overdueData?.by_provider?.exact ?? {};
|
|
121
|
+
const rev = revenueData?.by_provider;
|
|
122
|
+
const revFor = (provider, externalId) => {
|
|
123
|
+
const map = provider === "twinfield" ? rev?.twinfield
|
|
124
|
+
: provider === "exact" ? rev?.exact
|
|
125
|
+
: provider === "floriday" ? rev?.floriday
|
|
126
|
+
: provider === "local" ? rev?.local
|
|
127
|
+
: undefined;
|
|
128
|
+
return map?.[externalId];
|
|
129
|
+
};
|
|
130
|
+
const mergeRevenue = (into, hit) => {
|
|
131
|
+
if (!hit)
|
|
132
|
+
return into;
|
|
133
|
+
if (!into)
|
|
134
|
+
return { ...hit };
|
|
135
|
+
return {
|
|
136
|
+
count: into.count + hit.count,
|
|
137
|
+
amount: into.amount + hit.amount,
|
|
138
|
+
last_date: hit.last_date > into.last_date ? hit.last_date : into.last_date,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
111
141
|
// Verzamel lokale customer-id's die in de externe grouped view meemerged zijn
|
|
112
142
|
// met een andere provider (bijv. Twinfield/Floriday via BTW-nummer). Voor die
|
|
113
143
|
// klanten laten we de lokale rij vallen en gebruiken we de merged rij.
|
|
@@ -144,6 +174,7 @@ export function KlantenOverzicht() {
|
|
|
144
174
|
logo_url: c.logo_url ?? null,
|
|
145
175
|
is_active: !!c.is_active,
|
|
146
176
|
overdue: null,
|
|
177
|
+
revenue: mergeRevenue(mergeRevenue(null, revFor("local", String(c.id))), c.twinfield_code ? revFor("twinfield", String(c.twinfield_code)) : undefined),
|
|
147
178
|
price_groups: c.price_groups ?? [],
|
|
148
179
|
payment_method: derivePaymentMethod(null, c),
|
|
149
180
|
twinfield_code: c.twinfield_code ?? null,
|
|
@@ -160,6 +191,10 @@ export function KlantenOverzicht() {
|
|
|
160
191
|
const twinfieldSource = (c.sources ?? [])
|
|
161
192
|
.find((s) => s.provider === "twinfield");
|
|
162
193
|
let overdue = null;
|
|
194
|
+
let revenue = null;
|
|
195
|
+
for (const s of (c.sources ?? [])) {
|
|
196
|
+
revenue = mergeRevenue(revenue, revFor(s.provider, s.external_id));
|
|
197
|
+
}
|
|
163
198
|
for (const s of (c.sources ?? [])) {
|
|
164
199
|
const hit = s.provider === "twinfield" ? tw[s.external_id]
|
|
165
200
|
: s.provider === "exact" ? ex[s.external_id]
|
|
@@ -189,22 +224,24 @@ export function KlantenOverzicht() {
|
|
|
189
224
|
logo_url: c.logo_url,
|
|
190
225
|
is_active: c.is_active,
|
|
191
226
|
overdue,
|
|
227
|
+
revenue,
|
|
192
228
|
price_groups: localRow?.price_groups ?? [],
|
|
193
229
|
payment_method: derivePaymentMethod((c.sources ?? []), localRow),
|
|
194
230
|
twinfield_code: twinfieldSource?.external_id ?? localRow?.twinfield_code ?? null,
|
|
195
231
|
};
|
|
196
232
|
});
|
|
197
233
|
return [...local, ...ext];
|
|
198
|
-
}, [localData, externalData, overdueData]);
|
|
234
|
+
}, [localData, externalData, overdueData, revenueData]);
|
|
199
235
|
const totals = useMemo(() => {
|
|
200
236
|
const t = {
|
|
201
237
|
all: allRows.length,
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
// Actief = minimaal één factuur/order in de afgelopen 12 maanden.
|
|
239
|
+
active: loadingRevenue ? null : allRows.filter((c) => (c.revenue?.count ?? 0) > 0).length,
|
|
240
|
+
inactive: loadingRevenue ? null : allRows.filter((c) => (c.revenue?.count ?? 0) === 0).length,
|
|
204
241
|
overdue: allRows.filter((c) => (c.overdue?.count ?? 0) > 0).length,
|
|
205
242
|
};
|
|
206
243
|
return t;
|
|
207
|
-
}, [allRows]);
|
|
244
|
+
}, [allRows, loadingRevenue]);
|
|
208
245
|
const countries = useMemo(() => {
|
|
209
246
|
const set = new Set();
|
|
210
247
|
allRows.forEach((c) => {
|
|
@@ -247,9 +284,9 @@ export function KlantenOverzicht() {
|
|
|
247
284
|
const customers = useMemo(() => {
|
|
248
285
|
let rows = allRows;
|
|
249
286
|
if (view === "active")
|
|
250
|
-
rows = rows.filter((c) => c.
|
|
287
|
+
rows = rows.filter((c) => (c.revenue?.count ?? 0) > 0);
|
|
251
288
|
if (view === "inactive")
|
|
252
|
-
rows = rows.filter((c) =>
|
|
289
|
+
rows = rows.filter((c) => (c.revenue?.count ?? 0) === 0);
|
|
253
290
|
if (view === "overdue")
|
|
254
291
|
rows = rows.filter((c) => (c.overdue?.count ?? 0) > 0);
|
|
255
292
|
if (selectedClasses.length > 0)
|
|
@@ -328,7 +365,11 @@ export function KlantenOverzicht() {
|
|
|
328
365
|
});
|
|
329
366
|
const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
|
|
330
367
|
const lastUpdated = externalData?.last_fetched_at ?? null;
|
|
331
|
-
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip:
|
|
368
|
+
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: revenueData
|
|
369
|
+
? `Klanten met minimaal één bestelling of factuur sinds ${formatDateNl(revenueData.since)}.\nTotale omzet in die periode: ${formatEuro(revenueData.totals.amount)} · ${revenueData.totals.transactions} transacties`
|
|
370
|
+
: "Klanten met minimaal één bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: revenueData
|
|
371
|
+
? `Klanten zonder enige bestelling of factuur sinds ${formatDateNl(revenueData.since)}.`
|
|
372
|
+
: "Klanten zonder omzet in de afgelopen 12 maanden.", active: view === "inactive", onClick: () => { setView("inactive"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(AlertCircle, { className: "h-5 w-5" }), tint: "destructive", label: "Achterstallige betalingen", value: totals.overdue, tooltip: (overdueData?.totals
|
|
332
373
|
? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen`
|
|
333
374
|
: "Klanten met openstaande facturen waarvan de vervaldatum is verstreken."), active: view === "overdue", onClick: () => { setView("overdue"); resetPage(); } })] }), _jsxs(Card, { children: [_jsx("div", { className: "border-b px-4 pt-3", children: _jsx(Tabs, { value: view, onValueChange: (v) => { setView(v); resetPage(); }, children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.all ?? 0 })] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.active ?? 0 })] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.inactive ?? 0 })] }), _jsxs(TabsTrigger, { value: "overdue", children: ["Achterstallig ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.overdue ?? 0 })] })] }) }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap p-4 border-b", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { "aria-label": "Zoek klanten", placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: searchInput, onChange: (e) => setSearchInput(e.target.value), className: "pl-9" })] }), _jsx(MultiSearchableSelect, { values: selectedClasses, onValuesChange: (v) => { setSelectedClasses(v); resetPage(); }, options: classOptions, placeholder: "Alle klassen", triggerClassName: "w-40" }), _jsx(MultiSearchableSelect, { values: selectedCountries, onValuesChange: (v) => { setSelectedCountries(v); resetPage(); }, options: countryOptions, placeholder: "Alle landen", triggerClassName: "w-36" }), _jsx(MultiSearchableSelect, { values: selectedChannels, onValuesChange: (v) => { setSelectedChannels(v); resetPage(); }, options: channelSelectOptions, placeholder: "Alle kanalen", triggerClassName: "w-40" }), _jsx(MultiSearchableSelect, { values: selectedPayments, onValuesChange: (v) => { setSelectedPayments(v); resetPage(); }, options: paymentSelectOptions, placeholder: "Alle betaalvormen", triggerClassName: "w-40" })] }), activeFilterChips.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap px-4 py-2 border-b bg-muted/30", children: [activeFilterChips.map((chip) => (_jsxs("button", { type: "button", onClick: chip.clear, className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs hover:bg-muted", children: [chip.label, " ", _jsx(X, { className: "h-3 w-3" })] }, chip.key))), _jsx("button", { type: "button", onClick: () => {
|
|
334
375
|
setSearchInput("");
|
|
@@ -339,7 +380,7 @@ export function KlantenOverzicht() {
|
|
|
339
380
|
setSelectedPayments([]);
|
|
340
381
|
setView("all");
|
|
341
382
|
setPage(1);
|
|
342
|
-
}, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 8 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan:
|
|
383
|
+
}, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _jsx(TableHead, { children: "Omzet 12 mnd" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 8 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 9, children: _jsx(EmptyState, { onAdd: () => goTo({ to: "/klanten/nieuw" }), filtered: activeFilterChips.length > 0, onClearFilters: () => {
|
|
343
384
|
setSearchInput("");
|
|
344
385
|
setSearch("");
|
|
345
386
|
setSelectedClasses([]);
|
|
@@ -357,8 +398,15 @@ export function KlantenOverzicht() {
|
|
|
357
398
|
: goTo({
|
|
358
399
|
to: "/klanten/groep/$groupKey",
|
|
359
400
|
params: { groupKey: c.group_key },
|
|
360
|
-
}), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.overdue && c.overdue.count > 0 && (_jsxs("span", { title: `${c.overdue.count} openstaande factu${c.overdue.count === 1 ? "ur" : "ren"} · oudste vervallen op ${c.overdue.oldest_due}`, className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 text-destructive px-2 py-0.5 text-[11px] font-medium shrink-0", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", formatEuro(c.overdue.total), " achterstallig"] }))] }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.
|
|
361
|
-
|
|
401
|
+
}), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.overdue && c.overdue.count > 0 && (_jsxs("span", { title: `${c.overdue.count} openstaande factu${c.overdue.count === 1 ? "ur" : "ren"} · oudste vervallen op ${c.overdue.oldest_due}`, className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 text-destructive px-2 py-0.5 text-[11px] font-medium shrink-0", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", formatEuro(c.overdue.total), " achterstallig"] }))] }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm whitespace-nowrap", children: c.revenue && c.revenue.count > 0 ? (_jsxs("span", { title: `${c.revenue.count} transactie${c.revenue.count === 1 ? "" : "s"} · laatste op ${formatDateNl(c.revenue.last_date)}`, children: [_jsx("span", { className: "font-medium", children: formatEuro(c.revenue.amount) }), _jsxs("span", { className: "block text-xs text-muted-foreground", children: ["laatst ", formatDateNl(c.revenue.last_date)] })] })) : (_jsx("span", { className: "text-muted-foreground", children: "Geen omzet" })) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: formatCountry(c.country) })] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(StatusPill, { active: c.is_active }), c.id ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Klant bewerken", onClick: (e) => {
|
|
402
|
+
e.stopPropagation();
|
|
403
|
+
setEditTarget({ customerId: c.id });
|
|
404
|
+
}, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] })) : c.twinfield_code ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Twinfield-klant overnemen en bewerken", onClick: (e) => {
|
|
405
|
+
e.stopPropagation();
|
|
406
|
+
setEditTarget({ twinfieldCode: c.twinfield_code });
|
|
407
|
+
}, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] })) : null] }) })] }, c.key));
|
|
408
|
+
}) })] }) }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3 border-t text-sm flex-wrap", children: [_jsx("span", { className: "text-muted-foreground", children: isLoading ? "Laden..." : total === 0 ? "0 klanten" : `${startIdx + 1}–${endIdx} van ${total}` }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Per pagina" }), _jsxs(Select, { value: String(pageSize), onValueChange: (v) => { setPageSize(Number(v)); setPage(1); }, children: [_jsx(SelectTrigger, { className: "w-20 h-8", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "25", children: "25" }), _jsx(SelectItem, { value: "50", children: "50" }), _jsx(SelectItem, { value: "100", children: "100" })] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.max(1, p - 1)), disabled: currentPage <= 1, children: [_jsx(ChevronLeft, { className: "h-4 w-4 mr-1" }), " Vorige"] }), _jsxs("span", { className: "text-muted-foreground", children: [currentPage, " / ", totalPages] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.min(totalPages, p + 1)), disabled: currentPage >= totalPages, children: ["Volgende ", _jsx(ChevronRight, { className: "h-4 w-4 ml-1" })] })] })] })] }), _jsx(KlantBewerkenDialog, { open: editTarget !== null, onOpenChange: (open) => { if (!open)
|
|
409
|
+
setEditTarget(null); }, customerId: editTarget?.customerId ?? null, twinfieldCode: editTarget?.twinfieldCode ?? null, onSaved: () => queryClient.invalidateQueries() })] }) }));
|
|
362
410
|
}
|
|
363
411
|
function StatCard({ icon, label, value, active, onClick, tooltip, tint = "muted", disabled = false, }) {
|
|
364
412
|
const tintCls = {
|
|
@@ -406,6 +454,12 @@ function relativeTime(iso) {
|
|
|
406
454
|
return `${day} dag${day === 1 ? "" : "en"} geleden`;
|
|
407
455
|
return d.toLocaleDateString("nl-NL");
|
|
408
456
|
}
|
|
457
|
+
function formatDateNl(iso) {
|
|
458
|
+
const d = new Date(`${iso}T00:00:00`);
|
|
459
|
+
if (Number.isNaN(d.getTime()))
|
|
460
|
+
return iso;
|
|
461
|
+
return d.toLocaleDateString("nl-NL", { day: "numeric", month: "short", year: "numeric" });
|
|
462
|
+
}
|
|
409
463
|
function formatEuro(n) {
|
|
410
464
|
try {
|
|
411
465
|
return new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(n);
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* Neemt een Twinfield-debiteur over naar een lokale, bewerkbare klant. Bestaat
|
|
3
3
|
* de lokale klant al, dan sturen we meteen door naar het bewerkscherm.
|
|
4
4
|
*/
|
|
5
|
-
export declare function OvernemenTwinfieldKlant({ code }: {
|
|
5
|
+
export declare function OvernemenTwinfieldKlant({ code, onExistingLocal, onDone, onCancel, hideBackLink, }: {
|
|
6
6
|
code: string;
|
|
7
|
+
/** Bestaat er al een lokale klant, dan geeft dit het id door i.p.v. te navigeren. */
|
|
8
|
+
onExistingLocal?: (id: string) => void;
|
|
9
|
+
/** Na succesvol overnemen; navigeert dan zelf niet. */
|
|
10
|
+
onDone?: (customerId: string | null) => void;
|
|
11
|
+
onCancel?: () => void;
|
|
12
|
+
hideBackLink?: boolean;
|
|
7
13
|
}): import("react").JSX.Element;
|
|
8
14
|
//# sourceMappingURL=OvernemenTwinfieldKlant.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OvernemenTwinfieldKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/OvernemenTwinfieldKlant.tsx"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"OvernemenTwinfieldKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/OvernemenTwinfieldKlant.tsx"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,eAAe,EACf,MAAM,EACN,QAAQ,EACR,YAAY,GACb,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,uDAAuD;IACvD,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,+BAsGA"}
|
|
@@ -21,7 +21,7 @@ function num(v) {
|
|
|
21
21
|
* Neemt een Twinfield-debiteur over naar een lokale, bewerkbare klant. Bestaat
|
|
22
22
|
* de lokale klant al, dan sturen we meteen door naar het bewerkscherm.
|
|
23
23
|
*/
|
|
24
|
-
export function OvernemenTwinfieldKlant({ code }) {
|
|
24
|
+
export function OvernemenTwinfieldKlant({ code, onExistingLocal, onDone, onCancel, hideBackLink, }) {
|
|
25
25
|
const navigate = useNavigate();
|
|
26
26
|
const qc = useQueryClient();
|
|
27
27
|
const prepare = useServerFn(prepareTwinfieldAdoption);
|
|
@@ -32,9 +32,13 @@ export function OvernemenTwinfieldKlant({ code }) {
|
|
|
32
32
|
});
|
|
33
33
|
const existingId = data?.localId ?? null;
|
|
34
34
|
useEffect(() => {
|
|
35
|
-
if (existingId)
|
|
35
|
+
if (!existingId)
|
|
36
|
+
return;
|
|
37
|
+
if (onExistingLocal)
|
|
38
|
+
onExistingLocal(existingId);
|
|
39
|
+
else
|
|
36
40
|
navigate({ to: "/klanten/bewerken/$id", params: { id: existingId }, replace: true });
|
|
37
|
-
}, [existingId, navigate]);
|
|
41
|
+
}, [existingId, navigate, onExistingLocal]);
|
|
38
42
|
const mut = useMutation({
|
|
39
43
|
mutationFn: (values) => adopt({ data: { code, values } }),
|
|
40
44
|
onSuccess: (r) => {
|
|
@@ -44,8 +48,11 @@ export function OvernemenTwinfieldKlant({ code }) {
|
|
|
44
48
|
}
|
|
45
49
|
qc.invalidateQueries({ queryKey: ["customers-local"] });
|
|
46
50
|
qc.invalidateQueries({ queryKey: ["customers-external"] });
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
qc.invalidateQueries({ queryKey: ["customers"] });
|
|
52
|
+
const id = r.customer?.id ?? null;
|
|
53
|
+
if (onDone)
|
|
54
|
+
onDone(id);
|
|
55
|
+
else if (id)
|
|
49
56
|
navigate({ to: "/klanten/$id", params: { id } });
|
|
50
57
|
else
|
|
51
58
|
navigate({ to: "/klanten" });
|
|
@@ -55,7 +62,7 @@ export function OvernemenTwinfieldKlant({ code }) {
|
|
|
55
62
|
if (isLoading || existingId)
|
|
56
63
|
return _jsx("main", { className: "p-6", children: "Laden..." });
|
|
57
64
|
if (error || !data?.prefill) {
|
|
58
|
-
return (_jsxs("main", { className: "p-6 space-y-3", children: [_jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Deze Twinfield-klant is niet gevonden." })] }));
|
|
65
|
+
return (_jsxs("main", { className: "p-6 space-y-3", children: [!hideBackLink && _jsx(BackLink, {}), _jsx("p", { className: "text-sm text-destructive", children: error instanceof Error ? error.message : "Deze Twinfield-klant is niet gevonden." })] }));
|
|
59
66
|
}
|
|
60
67
|
const p = data.prefill;
|
|
61
68
|
const defaults = {
|
|
@@ -96,5 +103,5 @@ export function OvernemenTwinfieldKlant({ code }) {
|
|
|
96
103
|
customer_number: code,
|
|
97
104
|
force: true,
|
|
98
105
|
};
|
|
99
|
-
return (_jsx(KlantFormulier, { title: `${p.company_name || `Debiteur ${code}`} bewerken`, submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: defaults, numberReadOnly: true, numberHint: `Twinfield-debiteurnummer ${code} — niet aanpasbaar`, onSubmit: (values) => mut.mutate(values), onCancel: () => navigate({ to: "/klanten" }) }));
|
|
106
|
+
return (_jsx(KlantFormulier, { title: `${p.company_name || `Debiteur ${code}`} bewerken`, submitLabel: "Wijzigingen opslaan", submitting: mut.isPending, defaultValues: defaults, numberReadOnly: true, numberHint: `Twinfield-debiteurnummer ${code} — niet aanpasbaar`, onSubmit: (values) => mut.mutate(values), onCancel: () => (onCancel ? onCancel() : navigate({ to: "/klanten" })) }));
|
|
100
107
|
}
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { NieuweKlant } from "./components/klanten/NieuweKlant";
|
|
|
8
8
|
export { KlantFormulier } from "./components/klanten/KlantFormulier";
|
|
9
9
|
export { BewerkKlant } from "./components/klanten/BewerkKlant";
|
|
10
10
|
export { BewerkKlantRoute } from "./components/klanten/BewerkKlantRoute";
|
|
11
|
+
export { KlantBewerkenDialog } from "./components/klanten/KlantBewerkenDialog";
|
|
11
12
|
export { OvernemenTwinfieldKlant } from "./components/klanten/OvernemenTwinfieldKlant";
|
|
12
13
|
export { OvernemenTwinfieldKlantRoute } from "./components/klanten/OvernemenTwinfieldKlantRoute";
|
|
13
14
|
export { ImportKlanten } from "./components/klanten/ImportKlanten";
|
package/dist-lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,UAgBnC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qDAAqD,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,eAAO,MAAM,qBAAqB,EAAE,UAgBnC,CAAC"}
|
package/dist-lib/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { NieuweKlant } from "./components/klanten/NieuweKlant";
|
|
|
24
24
|
export { KlantFormulier } from "./components/klanten/KlantFormulier";
|
|
25
25
|
export { BewerkKlant } from "./components/klanten/BewerkKlant";
|
|
26
26
|
export { BewerkKlantRoute } from "./components/klanten/BewerkKlantRoute";
|
|
27
|
+
export { KlantBewerkenDialog } from "./components/klanten/KlantBewerkenDialog";
|
|
27
28
|
export { OvernemenTwinfieldKlant } from "./components/klanten/OvernemenTwinfieldKlant";
|
|
28
29
|
export { OvernemenTwinfieldKlantRoute } from "./components/klanten/OvernemenTwinfieldKlantRoute";
|
|
29
30
|
export { ImportKlanten } from "./components/klanten/ImportKlanten";
|