@flowselections/floriday-klanten-module 1.0.24 → 1.0.26
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/BewerkKlantRoute.d.ts +4 -1
- package/dist-lib/components/klanten/BewerkKlantRoute.d.ts.map +1 -1
- package/dist-lib/components/klanten/BewerkKlantRoute.js +7 -4
- package/dist-lib/components/klanten/ChannelBadges.d.ts +11 -0
- package/dist-lib/components/klanten/ChannelBadges.d.ts.map +1 -0
- package/dist-lib/components/klanten/ChannelBadges.js +21 -0
- package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/DeliveryLocationsTab.js +9 -1
- package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/GroupedKlantDetail.js +30 -5
- package/dist-lib/components/klanten/GroupedKlantDetailRoute.d.ts +4 -1
- package/dist-lib/components/klanten/GroupedKlantDetailRoute.d.ts.map +1 -1
- package/dist-lib/components/klanten/GroupedKlantDetailRoute.js +7 -4
- package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDetail.js +26 -3
- package/dist-lib/components/klanten/KlantDetailLayout.d.ts +4 -2
- package/dist-lib/components/klanten/KlantDetailLayout.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDetailLayout.js +24 -22
- package/dist-lib/components/klanten/KlantDetailRoute.d.ts +4 -1
- package/dist-lib/components/klanten/KlantDetailRoute.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDetailRoute.js +7 -4
- package/dist-lib/components/klanten/KlantFormulier.d.ts +1 -1
- package/dist-lib/components/klanten/KlantFormulier.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantFormulier.js +56 -9
- package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenOverzicht.js +103 -16
- package/dist-lib/components/klanten/MultiSearchableSelect.d.ts +21 -0
- package/dist-lib/components/klanten/MultiSearchableSelect.d.ts.map +1 -0
- package/dist-lib/components/klanten/MultiSearchableSelect.js +86 -0
- package/dist-lib/components/klanten/NetwerkPagina.d.ts.map +1 -1
- package/dist-lib/components/klanten/NetwerkPagina.js +2 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.js +1 -3
- package/dist-lib/components/klanten/OvernemenTwinfieldKlantRoute.d.ts +4 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlantRoute.d.ts.map +1 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlantRoute.js +7 -4
- package/dist-lib/components/klanten/SearchableSelect.d.ts +17 -0
- package/dist-lib/components/klanten/SearchableSelect.d.ts.map +1 -0
- package/dist-lib/components/klanten/SearchableSelect.js +57 -0
- package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.d.ts.map +1 -1
- package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.js +3 -2
- package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts.map +1 -1
- package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.js +2 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +33 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +33 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +33 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +35 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +99 -0
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting.functions.d.ts +99 -0
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/countries.d.ts +31 -0
- package/dist-lib/lib/countries.d.ts.map +1 -0
- package/dist-lib/lib/countries.js +194 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts +33 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-notes.functions.d.ts +39638 -0
- package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -0
- package/dist-lib/lib/customer-notes.functions.js +59 -0
- package/dist-lib/lib/customers.functions.d.ts +297 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/delivery-locations.functions.d.ts +132 -0
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.d.ts +132 -0
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-assortment.functions.d.ts +33 -0
- package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-discounts.functions.d.ts +66 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-trade-items.functions.d.ts +33 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
- package/dist-lib/lib/local-discounts.functions.d.ts +231 -0
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-events.functions.d.ts +33 -0
- package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-strategy.functions.d.ts +132 -0
- package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
- package/dist-lib/lib/overdue-invoices.functions.d.ts +33 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
- package/dist-lib/lib/payment-method.d.ts +22 -0
- package/dist-lib/lib/payment-method.d.ts.map +1 -0
- package/dist-lib/lib/payment-method.js +74 -0
- package/dist-lib/lib/price-groups-local.functions.d.ts +231 -0
- package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups.functions.d.ts +33 -0
- package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adopt.functions.d.ts +66 -0
- package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.functions.d.ts +33 -0
- package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
- package/package.json +1 -1
- package/public/flowselections-assets/Klanten/twinfield-logo.svg +218 -0
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Route-onafhankelijk: leest `$id` uit welke route de shell ook gebruikt. */
|
|
2
|
+
export declare function BewerkKlantRoute({ id: propId }?: {
|
|
3
|
+
id?: string;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
2
5
|
//# sourceMappingURL=BewerkKlantRoute.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BewerkKlantRoute.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlantRoute.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BewerkKlantRoute.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlantRoute.tsx"],"names":[],"mappings":"AAGA,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAO,+BAKpE"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useParams } from "@tanstack/react-router";
|
|
3
3
|
import { BewerkKlant } from "./BewerkKlant";
|
|
4
|
-
|
|
5
|
-
export function BewerkKlantRoute() {
|
|
6
|
-
const
|
|
4
|
+
/** Route-onafhankelijk: leest `$id` uit welke route de shell ook gebruikt. */
|
|
5
|
+
export function BewerkKlantRoute({ id: propId } = {}) {
|
|
6
|
+
const params = useParams({ strict: false });
|
|
7
|
+
const id = propId ?? params.id;
|
|
8
|
+
if (!id)
|
|
9
|
+
return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Geen klant geselecteerd." });
|
|
7
10
|
return _jsx(BewerkKlant, { id: id });
|
|
8
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toont per klant uit welk kanaal (bron) de gegevens komen: Floriday, Twinfield
|
|
3
|
+
* of een andere gekoppelde provider. Logo + naam, in gelijke grootte.
|
|
4
|
+
*/
|
|
5
|
+
type Props = {
|
|
6
|
+
providers: string[] | null | undefined;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function ChannelBadges({ providers, className }: Props): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=ChannelBadges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChannelBadges.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ChannelBadges.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,KAAK,KAAK,GAAG;IACX,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,KAAK,+BAqB5D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Toont per klant uit welk kanaal (bron) de gegevens komen: Floriday, Twinfield
|
|
4
|
+
* of een andere gekoppelde provider. Logo + naam, in gelijke grootte.
|
|
5
|
+
*/
|
|
6
|
+
const FLORIDAY_LOGO = "/flowselections-assets/Klanten/floriday-logo.png";
|
|
7
|
+
const TWINFIELD_LOGO = "/flowselections-assets/Klanten/twinfield-logo.svg";
|
|
8
|
+
const CHANNELS = {
|
|
9
|
+
floriday: { label: "Floriday", logo: FLORIDAY_LOGO },
|
|
10
|
+
twinfield: { label: "Twinfield", logo: TWINFIELD_LOGO },
|
|
11
|
+
exact: { label: "Exact" },
|
|
12
|
+
};
|
|
13
|
+
export function ChannelBadges({ providers, className }) {
|
|
14
|
+
const list = (providers ?? []).filter((p) => p && p !== "local");
|
|
15
|
+
if (list.length === 0)
|
|
16
|
+
return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
17
|
+
return (_jsx("div", { className: `flex items-center gap-1.5 flex-wrap ${className ?? ""}`, children: list.map((p) => {
|
|
18
|
+
const meta = CHANNELS[p] ?? { label: p.charAt(0).toUpperCase() + p.slice(1) };
|
|
19
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-2 py-0.5 text-xs", children: [meta.logo ? (_jsx("img", { src: meta.logo, alt: "", "aria-hidden": "true", className: "h-4 w-4 object-contain" })) : null, _jsx("span", { children: meta.label })] }, p));
|
|
20
|
+
}) }));
|
|
21
|
+
}
|
|
@@ -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":"AA0DA,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,+BA4UA"}
|
|
@@ -7,6 +7,7 @@ import { MapPin, Plus, Pencil, Trash2, Truck, Sparkles, PackageCheck } from "luc
|
|
|
7
7
|
import { listCarriers, listDeliveryLocations, upsertDeliveryLocation, deleteDeliveryLocation, pickCarrierForLocation, } from "../../lib/delivery-locations.functions";
|
|
8
8
|
import { EU_COUNTRIES, findCountry } from "../../lib/eu-regions";
|
|
9
9
|
import { decodeDeep } from "../../lib/decode-html";
|
|
10
|
+
import { countryNameToFlag } from "../../lib/countries";
|
|
10
11
|
const EMPTY = {
|
|
11
12
|
name: "", street: "", house_number: "", postal_code: "",
|
|
12
13
|
city: "", province: "", country: "Nederland",
|
|
@@ -85,7 +86,14 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
85
86
|
qc.invalidateQueries({ queryKey: ["customer", ownerKey] });
|
|
86
87
|
},
|
|
87
88
|
});
|
|
88
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: "Afleverlocaties staan volledig los van de boekhoudlocatie uit Twinfield en worden alleen gebruikt voor orderverwerking, planning, picking en levering. De vervoerder wordt automatisch gekoppeld op basis van het bedieningsgebied." }), _jsxs(Button, { size: "sm", onClick: startNew, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Locatie toevoegen"] })] }), locations.length === 0 ? (_jsx(Card, { children: _jsx(CardContent, { className: "py-10 text-center text-sm text-muted-foreground", children: "Nog geen afleverlocaties." }) })) : (_jsx("div", { className: "grid gap-3 md:grid-cols-2", children: locations.map((loc) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-5 space-y-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-semibold flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), loc.name || [loc.street, loc.house_number].filter(Boolean).join(" ") || "Afleverlocatie"] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-0.5", children: [[loc.street, loc.house_number].filter(Boolean).join(" "), (loc.street || loc.house_number) && (loc.postal_code || loc.city) ? ", " : "", [loc.postal_code, loc.city].filter(Boolean).join(" "), loc.province ? ` · ${loc.province}` : "", loc.country ?
|
|
89
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: "Afleverlocaties staan volledig los van de boekhoudlocatie uit Twinfield en worden alleen gebruikt voor orderverwerking, planning, picking en levering. De vervoerder wordt automatisch gekoppeld op basis van het bedieningsgebied." }), _jsxs(Button, { size: "sm", onClick: startNew, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Locatie toevoegen"] })] }), locations.length === 0 ? (_jsx(Card, { children: _jsx(CardContent, { className: "py-10 text-center text-sm text-muted-foreground", children: "Nog geen afleverlocaties." }) })) : (_jsx("div", { className: "grid gap-3 md:grid-cols-2", children: locations.map((loc) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-5 space-y-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-semibold flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), loc.name || [loc.street, loc.house_number].filter(Boolean).join(" ") || "Afleverlocatie"] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-0.5", children: [[loc.street, loc.house_number].filter(Boolean).join(" "), (loc.street || loc.house_number) && (loc.postal_code || loc.city) ? ", " : "", [loc.postal_code, loc.city].filter(Boolean).join(" "), loc.province ? ` · ${loc.province}` : "", loc.country ? (_jsxs("span", { className: "inline-flex items-center gap-1 ml-1", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(loc.country) ?? "🌐" }), _jsx("span", { children: loc.country })] })) : null] })] }), _jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => startEdit(loc), children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", children: _jsx(Trash2, { className: "h-4 w-4" }) }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Locatie verwijderen?" }), _jsx(AlertDialogDescription, { children: "Deze actie kan niet ongedaan worden gemaakt." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(loc.id), children: "Verwijderen" })] })] })] })] })] }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap text-sm", children: [loc.self_pickup ? (_jsxs(_Fragment, { children: [_jsx(PackageCheck, { className: "h-4 w-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: "Eigen vervoer" }), _jsx(Badge, { variant: "secondary", children: "Klant haalt zelf op" })] })) : (_jsxs(_Fragment, { children: [_jsx(Truck, { className: "h-4 w-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: carrierName(loc.carrier_id) }), loc.carrier_assignment_source === "auto" && loc.carrier_id && (_jsxs(Badge, { variant: "secondary", className: "gap-1", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "Automatisch"] })), loc.carrier_assignment_source === "manual" && (_jsx(Badge, { variant: "outline", children: "Handmatig" })), !loc.carrier_id && (_jsx(Badge, { variant: "outline", className: "text-amber-600 border-amber-300", children: "Geen match" }))] })), loc.is_primary && _jsx(Badge, { children: "Vaste locatie" })] })] }) }, loc.id))) })), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: form.id ? "Locatie bewerken" : "Nieuwe locatie" }), _jsx(DialogDescription, { children: "De vervoerder wordt automatisch gekozen op basis van plaats, provincie of land." })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { children: "Locatienaam (optioneel)" }), _jsx(Input, { value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }) })] }), _jsxs("div", { className: "md:col-span-2 grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "col-span-2", children: [_jsx(Label, { children: "Straat" }), _jsx(Input, { value: form.street, onChange: (e) => setForm({ ...form, street: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Huisnummer" }), _jsx(Input, { value: form.house_number, onChange: (e) => setForm({ ...form, house_number: e.target.value }) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Postcode" }), _jsx(Input, { value: form.postal_code, onChange: (e) => setForm({ ...form, postal_code: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Plaats" }), _jsx(Input, { value: form.city, onChange: (e) => setForm({ ...form, city: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Land" }), _jsxs(Select, { value: findCountry(form.country)?.name ?? "", onValueChange: (v) => setForm({ ...form, country: v, province: "" }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een land", children: (() => {
|
|
90
|
+
const selectedName = findCountry(form.country)?.name ?? "";
|
|
91
|
+
const flag = countryNameToFlag(selectedName);
|
|
92
|
+
return selectedName ? (_jsxs("span", { className: "inline-flex items-center gap-2", children: [flag ? _jsx("span", { className: "text-base", "aria-hidden": "true", children: flag }) : _jsx("span", { className: "w-4" }), _jsx("span", { children: selectedName })] })) : null;
|
|
93
|
+
})() }) }), _jsx(SelectContent, { children: EU_COUNTRIES.map((c) => {
|
|
94
|
+
const flag = countryNameToFlag(c.code);
|
|
95
|
+
return (_jsx(SelectItem, { value: c.name, children: _jsxs("span", { className: "inline-flex items-center gap-2", children: [flag ? _jsx("span", { className: "text-base", "aria-hidden": "true", children: flag }) : _jsx("span", { className: "w-4" }), _jsx("span", { children: c.name })] }) }, c.code));
|
|
96
|
+
}) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Provincie" }), (() => {
|
|
89
97
|
const provinces = findCountry(form.country)?.provinces ?? [];
|
|
90
98
|
return provinces.length > 0 ? (_jsxs(Select, { value: form.province || "", onValueChange: (v) => setForm({ ...form, province: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een provincie" }) }), _jsx(SelectContent, { children: provinces.map((p) => (_jsx(SelectItem, { value: p, children: p }, p))) })] })) : (_jsx(Input, { value: form.province, onChange: (e) => setForm({ ...form, province: e.target.value }), placeholder: "Kies eerst een land", disabled: !form.country }));
|
|
91
99
|
})()] }), _jsxs("div", { className: "md:col-span-2 space-y-2", children: [_jsx(Label, { children: "Vervoerder" }), _jsxs(Select, { value: form.self_pickup ? "__self__" : (form.carrier_id ?? "__none__"), onValueChange: (v) => {
|
|
@@ -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":"AAiCA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA4TpE"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useNavigate, Link } from "@tanstack/react-router";
|
|
3
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import { Card, CardContent, Badge, Button, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { Card, CardContent, Badge, Button, toast, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
7
7
|
import { Mail, MapPin, Phone, Pencil } from "lucide-react";
|
|
8
8
|
import { BackLink } from "../../components/klanten/BackLink";
|
|
9
9
|
import { getGroupedCustomerDetail, } from "../../lib/external-customers.functions";
|
|
10
10
|
import { ProfileCard, KpiStrip, BadgeList, EmptyState, adaptGroupedToProfile, } from "../../components/klanten/KlantDetailLayout";
|
|
11
|
+
import { countryNameToFlag } from "../../lib/countries";
|
|
11
12
|
import { DiscountsTab } from "../../components/klanten/DiscountsTab";
|
|
12
13
|
import { PriceGroupBadges } from "../../components/klanten/PriceGroupBadges";
|
|
13
14
|
import { listPriceGroupsForOrganizations } from "../../lib/price-groups.functions";
|
|
@@ -16,10 +17,15 @@ import { FloridayAssortmentTab } from "../../components/klanten/FloridayAssortme
|
|
|
16
17
|
import { decodeDeep } from "../../lib/decode-html";
|
|
17
18
|
import { LogisticsStrategyCard } from "../../components/klanten/LogisticsStrategyCard";
|
|
18
19
|
import { DeliveryLocationsTab } from "../../components/klanten/DeliveryLocationsTab";
|
|
20
|
+
import { getCustomerNote, upsertCustomerNote } from "../../lib/customer-notes.functions";
|
|
21
|
+
import { supabase } from "../../integrations/supabase/client";
|
|
19
22
|
export function GroupedKlantDetail({ groupKey }) {
|
|
20
23
|
const navigate = useNavigate();
|
|
24
|
+
const qc = useQueryClient();
|
|
21
25
|
const get = useServerFn(getGroupedCustomerDetail);
|
|
22
26
|
const listPg = useServerFn(listPriceGroupsForOrganizations);
|
|
27
|
+
const getNote = useServerFn(getCustomerNote);
|
|
28
|
+
const saveNote = useServerFn(upsertCustomerNote);
|
|
23
29
|
const [tab, setTab] = useState("overview");
|
|
24
30
|
const { data, isLoading, error } = useQuery({
|
|
25
31
|
queryKey: ["grouped-customer", groupKey],
|
|
@@ -35,6 +41,25 @@ export function GroupedKlantDetail({ groupKey }) {
|
|
|
35
41
|
}).then(decodeDeep),
|
|
36
42
|
enabled: !!floridayBuyerId,
|
|
37
43
|
});
|
|
44
|
+
const { data: noteData, isLoading: noteLoading } = useQuery({
|
|
45
|
+
queryKey: ["customer-note", "group_key", groupKey],
|
|
46
|
+
queryFn: () => getNote({ data: { group_key: groupKey } }),
|
|
47
|
+
});
|
|
48
|
+
const noteMut = useMutation({
|
|
49
|
+
mutationFn: (notes) => saveNote({ data: { group_key: groupKey, notes } }),
|
|
50
|
+
onSuccess: () => {
|
|
51
|
+
qc.invalidateQueries({ queryKey: ["customer-note", "group_key", groupKey] });
|
|
52
|
+
toast.success("Notitie opgeslagen");
|
|
53
|
+
},
|
|
54
|
+
onError: () => toast.error("Notitie opslaan mislukt"),
|
|
55
|
+
});
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const channel = supabase
|
|
58
|
+
.channel(`group-notes-${groupKey}`)
|
|
59
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "customer_notes" }, () => qc.invalidateQueries({ queryKey: ["customer-note", "group_key", groupKey] }))
|
|
60
|
+
.subscribe();
|
|
61
|
+
return () => { supabase.removeChannel(channel); };
|
|
62
|
+
}, [groupKey, qc]);
|
|
38
63
|
if (isLoading)
|
|
39
64
|
return _jsx("main", { className: "p-6", children: "Laden..." });
|
|
40
65
|
if (error || !data) {
|
|
@@ -64,12 +89,12 @@ export function GroupedKlantDetail({ groupKey }) {
|
|
|
64
89
|
provider: providerMap.get(s.provider)?.label ?? s.provider,
|
|
65
90
|
}))
|
|
66
91
|
.sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
|
|
67
|
-
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id: localCustomerId }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) })) : twinfieldCode ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/overnemen/$code", params: { code: twinfieldCode }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile }), _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: {
|
|
92
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id: localCustomerId }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) })) : twinfieldCode ? (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/overnemen/$code", params: { code: twinfieldCode }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
|
|
68
93
|
contacts: contacts.length,
|
|
69
94
|
locations: locations.length,
|
|
70
95
|
assortment: 0,
|
|
71
96
|
orders: 0,
|
|
72
|
-
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [_jsx(LogisticsStrategyCard, { scope: "group", refKey: groupKey, description: "Geldt voor alle bronnen binnen deze klantgroep. Een strategie op een individuele klant heeft voorrang." }), (segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Afleverlocaties (logistiek)"] }), _jsx(DeliveryLocationsTab, { groupKey: groupKey, customerId: localCustomerId })] }), locations.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Boekhoudlocaties uit de koppeling (read-only)"] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }),
|
|
97
|
+
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [_jsx(LogisticsStrategyCard, { scope: "group", refKey: groupKey, description: "Geldt voor alle bronnen binnen deze klantgroep. Een strategie op een individuele klant heeft voorrang." }), (segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Afleverlocaties (logistiek)"] }), _jsx(DeliveryLocationsTab, { groupKey: groupKey, customerId: localCustomerId })] }), locations.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Boekhoudlocaties uit de koppeling (read-only)"] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsxs(TableCell, { className: "text-sm", children: [l.postal_code ? _jsx("span", { className: "font-mono mr-1", children: l.postal_code }) : null, l.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(l.country) ?? "🌐" }), _jsx("span", { children: l.country })] })) : null, !l.postal_code && !l.country ? "—" : null] }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })] }))] }) }), _jsx(TabsContent, { value: "assortment", children: floridayBuyerId ? (_jsx(FloridayAssortmentTab, { organizationId: floridayBuyerId })) : (_jsx(EmptyState, { message: "Assortiment is beschikbaar zodra de koppeling dit levert." })) }), _jsx(TabsContent, { value: "orders", children: _jsx(EmptyState, { message: "Bestellingen zijn beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "trade", children: _jsx(EmptyState, { message: "Handelsinstellingen zijn niet beschikbaar via deze koppeling." }) }), floridayBuyerId && (_jsx(TabsContent, { value: "discounts", children: _jsx(DiscountsTab, { buyerOrganizationId: floridayBuyerId }) })), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: activity.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (activity.map((a) => (_jsxs("div", { className: "flex items-center justify-between text-sm border-l-2 border-primary pl-3 py-1", children: [_jsxs("span", { children: ["Bijgewerkt via ", _jsx("strong", { children: a.provider })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(a.created_at).toLocaleString("nl-NL") })] }, a.id)))) }) }) }), g.is_merged && sources.length > 1 && (_jsx(TabsContent, { value: "sources", children: _jsx(CompareCard, { sources: sources, providerMap: providerMap }) }))] })] })] })] }));
|
|
73
98
|
}
|
|
74
99
|
function collectContacts(sources, providerMap) {
|
|
75
100
|
const out = [];
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Route-onafhankelijk: leest `$groupKey` uit welke route de shell ook gebruikt. */
|
|
2
|
+
export declare function GroupedKlantDetailRoute({ groupKey: propKey }?: {
|
|
3
|
+
groupKey?: string;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
2
5
|
//# sourceMappingURL=GroupedKlantDetailRoute.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupedKlantDetailRoute.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetailRoute.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GroupedKlantDetailRoute.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetailRoute.tsx"],"names":[],"mappings":"AAGA,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,+BAKxF"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useParams } from "@tanstack/react-router";
|
|
3
3
|
import { GroupedKlantDetail } from "./GroupedKlantDetail";
|
|
4
|
-
|
|
5
|
-
export function GroupedKlantDetailRoute() {
|
|
6
|
-
const
|
|
4
|
+
/** Route-onafhankelijk: leest `$groupKey` uit welke route de shell ook gebruikt. */
|
|
5
|
+
export function GroupedKlantDetailRoute({ groupKey: propKey } = {}) {
|
|
6
|
+
const params = useParams({ strict: false });
|
|
7
|
+
const groupKey = propKey ?? params.groupKey;
|
|
8
|
+
if (!groupKey)
|
|
9
|
+
return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Geen klant geselecteerd." });
|
|
7
10
|
return _jsx(GroupedKlantDetail, { groupKey: groupKey });
|
|
8
11
|
}
|
|
@@ -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":"AAmCA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAsTjD"}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Link, useNavigate } from "@tanstack/react-router";
|
|
3
3
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import { useState } from "react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
6
|
import { Card, CardContent, CardHeader, CardTitle, Badge, Button, toast, Tabs, TabsContent, TabsList, TabsTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@flowselections/core";
|
|
7
7
|
import { Trash2, Mail, Phone, Power, Pencil, } from "lucide-react";
|
|
8
8
|
import { getCustomer, deleteCustomer, updateCustomer } from "../../lib/customers.functions";
|
|
@@ -18,17 +18,40 @@ import { SyncStatusChip } from "../../components/klanten/SyncStatusChip";
|
|
|
18
18
|
import { LogisticsStrategyCard } from "../../components/klanten/LogisticsStrategyCard";
|
|
19
19
|
import { decodeDeep } from "../../lib/decode-html";
|
|
20
20
|
import { getMissingTwinfieldFields } from "../../lib/twinfield-required-fields";
|
|
21
|
+
import { getCustomerNote, upsertCustomerNote } from "../../lib/customer-notes.functions";
|
|
22
|
+
import { supabase } from "../../integrations/supabase/client";
|
|
21
23
|
export function KlantDetail({ id }) {
|
|
22
24
|
const navigate = useNavigate();
|
|
23
25
|
const qc = useQueryClient();
|
|
24
26
|
const get = useServerFn(getCustomer);
|
|
25
27
|
const del = useServerFn(deleteCustomer);
|
|
26
28
|
const upd = useServerFn(updateCustomer);
|
|
29
|
+
const getNote = useServerFn(getCustomerNote);
|
|
30
|
+
const saveNote = useServerFn(upsertCustomerNote);
|
|
27
31
|
const [tab, setTab] = useState("overview");
|
|
28
32
|
const { data, isLoading } = useQuery({
|
|
29
33
|
queryKey: ["customer", id],
|
|
30
34
|
queryFn: () => get({ data: { id } }).then(decodeDeep),
|
|
31
35
|
});
|
|
36
|
+
const { data: noteData, isLoading: noteLoading } = useQuery({
|
|
37
|
+
queryKey: ["customer-note", "customer_id", id],
|
|
38
|
+
queryFn: () => getNote({ data: { customer_id: id } }),
|
|
39
|
+
});
|
|
40
|
+
const noteMut = useMutation({
|
|
41
|
+
mutationFn: (notes) => saveNote({ data: { customer_id: id, notes } }),
|
|
42
|
+
onSuccess: () => {
|
|
43
|
+
qc.invalidateQueries({ queryKey: ["customer-note", "customer_id", id] });
|
|
44
|
+
toast.success("Notitie opgeslagen");
|
|
45
|
+
},
|
|
46
|
+
onError: (err) => toast.error("Notitie opslaan mislukt"),
|
|
47
|
+
});
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const channel = supabase
|
|
50
|
+
.channel(`customer-notes-${id}`)
|
|
51
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "customer_notes" }, () => qc.invalidateQueries({ queryKey: ["customer-note", "customer_id", id] }))
|
|
52
|
+
.subscribe();
|
|
53
|
+
return () => { supabase.removeChannel(channel); };
|
|
54
|
+
}, [id, qc]);
|
|
32
55
|
const delMut = useMutation({
|
|
33
56
|
mutationFn: () => del({ data: { id } }),
|
|
34
57
|
onSuccess: () => { toast.success("Verwijderd"); navigate({ to: "/klanten" }); },
|
|
@@ -47,10 +70,10 @@ export function KlantDetail({ id }) {
|
|
|
47
70
|
const c = data.customer;
|
|
48
71
|
const missingTwinfield = getMissingTwinfieldFields(c);
|
|
49
72
|
const priceGroups = data.price_groups ?? c.price_groups ?? [];
|
|
50
|
-
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts) }), _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: {
|
|
73
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id }, children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] })), _jsx(KpiStrip, { counts: {
|
|
51
74
|
contacts: data.contacts.length,
|
|
52
75
|
locations: data.delivery_locations.length,
|
|
53
76
|
assortment: data.assortment.length,
|
|
54
77
|
orders: data.orders.length,
|
|
55
|
-
}, 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 }) })] })),
|
|
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 }) })] })] })] })] }));
|
|
56
79
|
}
|
|
@@ -23,12 +23,14 @@ export type CustomerProfile = {
|
|
|
23
23
|
phone_from_contact: boolean;
|
|
24
24
|
has_contacts: boolean;
|
|
25
25
|
};
|
|
26
|
-
export declare function formatCountry(value: string | null | undefined): string | null;
|
|
27
26
|
export declare function adaptLocalToProfile(c: any, contacts?: any[]): CustomerProfile;
|
|
28
27
|
export declare function adaptGroupedToProfile(g: GroupedCustomer): CustomerProfile;
|
|
29
28
|
export declare function adaptExternalToProfile(c: ExternalCustomer): CustomerProfile;
|
|
30
|
-
export declare function ProfileCard({ profile: p }: {
|
|
29
|
+
export declare function ProfileCard({ profile: p, notes, onSaveNotes, isSaving, }: {
|
|
31
30
|
profile: CustomerProfile;
|
|
31
|
+
notes?: string | null;
|
|
32
|
+
onSaveNotes?: (notes: string) => void | Promise<void>;
|
|
33
|
+
isSaving?: boolean;
|
|
32
34
|
}): import("react").JSX.Element;
|
|
33
35
|
export declare function SectionLabel({ children }: {
|
|
34
36
|
children: ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantDetailLayout.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetailLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"KlantDetailLayout.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetailLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAW5D,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAChB,MAAM,wCAAwC,CAAC;AAGhD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AASF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAE,GAAG,EAAO,GAAG,eAAe,CAgCjF;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,eAAe,GAAG,eAAe,CAqCzE;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,gBAAgB,GAAG,eAAe,CA+B3E;AAED,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EAAE,CAAC,EACV,KAAK,EACL,WAAW,EACX,QAAgB,GACjB,EAAE;IACD,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,+BAwLA;AAGD,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEjE;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,+BAWjI;AAED,wBAAgB,OAAO,CAAC,EACtB,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAC/C,EAAE;IACD,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACrI,+BAyBA;AAED,wBAAgB,QAAQ,CAAC,EACvB,MAAM,EAAE,QAAQ,GACjB,EAAE;IACD,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpF,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC,+BA6BA;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,+BAiBrG;AAED,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,+BAgBhG;AAED,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,+BAM1D"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { Card, CardContent, Badge, Separator, } from "@flowselections/core";
|
|
4
|
-
import { Mail, Phone, Globe, MapPin, Hash, Building2, ChevronDown, ChevronRight, Users, Truck, Package, ShoppingCart, } from "lucide-react";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Card, CardContent, Badge, Separator, Textarea, Button, } from "@flowselections/core";
|
|
4
|
+
import { Mail, Phone, Globe, MapPin, Hash, Building2, ChevronDown, ChevronRight, Users, Truck, Package, ShoppingCart, StickyNote, Pencil, Check, X, } from "lucide-react";
|
|
5
5
|
import { CustomerAvatar } from "../CustomerAvatar";
|
|
6
|
+
import { formatCountry, countryNameToFlag } from "../../lib/countries";
|
|
6
7
|
function firstNonEmpty(values) {
|
|
7
8
|
for (const v of values) {
|
|
8
9
|
if (v != null && (typeof v !== "string" || v.trim() !== ""))
|
|
@@ -10,21 +11,6 @@ function firstNonEmpty(values) {
|
|
|
10
11
|
}
|
|
11
12
|
return null;
|
|
12
13
|
}
|
|
13
|
-
const COUNTRY_NAMES = {
|
|
14
|
-
NL: "Nederland", BE: "België", DE: "Duitsland", FR: "Frankrijk",
|
|
15
|
-
ES: "Spanje", IT: "Italië", GB: "Verenigd Koninkrijk", UK: "Verenigd Koninkrijk",
|
|
16
|
-
PL: "Polen", DK: "Denemarken", SE: "Zweden", NO: "Noorwegen",
|
|
17
|
-
AT: "Oostenrijk", CH: "Zwitserland", PT: "Portugal", IE: "Ierland",
|
|
18
|
-
LU: "Luxemburg", CZ: "Tsjechië", FI: "Finland", US: "Verenigde Staten",
|
|
19
|
-
};
|
|
20
|
-
export function formatCountry(value) {
|
|
21
|
-
if (!value)
|
|
22
|
-
return null;
|
|
23
|
-
const trimmed = value.trim();
|
|
24
|
-
if (trimmed.length === 2)
|
|
25
|
-
return COUNTRY_NAMES[trimmed.toUpperCase()] ?? trimmed;
|
|
26
|
-
return trimmed;
|
|
27
|
-
}
|
|
28
14
|
export function adaptLocalToProfile(c, contacts = []) {
|
|
29
15
|
const primary = contacts.find((x) => x.is_active !== false) ?? contacts[0] ?? null;
|
|
30
16
|
const email = c.email ?? primary?.email ?? null;
|
|
@@ -120,13 +106,29 @@ export function adaptExternalToProfile(c) {
|
|
|
120
106
|
has_contacts: !!c.contact_person,
|
|
121
107
|
};
|
|
122
108
|
}
|
|
123
|
-
export function ProfileCard({ profile: p }) {
|
|
109
|
+
export function ProfileCard({ profile: p, notes, onSaveNotes, isSaving = false, }) {
|
|
110
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
111
|
+
const [draft, setDraft] = useState(notes ?? "");
|
|
124
112
|
const address = [p.street, p.house_number].filter(Boolean).join(" ");
|
|
125
113
|
const cityLine = [p.postal_code, p.city].filter(Boolean).join(" ");
|
|
126
|
-
const
|
|
114
|
+
const countryFlag = countryNameToFlag(p.country);
|
|
115
|
+
const countryDisplay = countryFlag ? `${countryFlag} ${p.country}` : p.country;
|
|
116
|
+
const fullAddress = [address, cityLine, countryDisplay].filter(Boolean).join(", ");
|
|
127
117
|
const [showTech, setShowTech] = useState(false);
|
|
128
118
|
const hasTech = !!(p.gln || p.rfh_id || p.organization_type);
|
|
129
|
-
|
|
119
|
+
const editable = !!onSaveNotes;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!isEditing)
|
|
122
|
+
setDraft(notes ?? "");
|
|
123
|
+
}, [notes, isEditing]);
|
|
124
|
+
const handleSave = async () => {
|
|
125
|
+
await onSaveNotes?.(draft);
|
|
126
|
+
setIsEditing(false);
|
|
127
|
+
};
|
|
128
|
+
return (_jsx(Card, { className: "lg:sticky lg:top-6 self-start", children: _jsxs(CardContent, { className: "pt-6 space-y-5", children: [_jsxs("div", { className: "flex flex-col items-center text-center", children: [_jsx(CustomerAvatar, { logoUrl: p.logo_url, name: p.company_name, size: "lg", className: "h-24 w-24 text-2xl" }), _jsx("h1", { className: "mt-3 text-xl font-bold leading-tight", children: p.company_name }), p.commercial_name && p.commercial_name !== p.company_name && (_jsx("p", { className: "text-sm text-muted-foreground", children: p.commercial_name })), _jsxs("div", { className: "mt-3 flex flex-wrap items-center justify-center gap-1.5", children: [p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "destructive", children: "Inactief" }), p.class_badge && _jsx(Badge, { variant: "secondary", children: p.class_badge })] })] }), (p.email || p.phone || p.website) && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { children: [_jsx(SectionLabel, { children: "Snelle acties" }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsx(QuickAction, { icon: _jsx(Mail, { className: "h-4 w-4" }), label: "E-mail", href: p.email ? `mailto:${p.email}` : undefined }), _jsx(QuickAction, { icon: _jsx(Phone, { className: "h-4 w-4" }), label: "Bellen", href: p.phone ? `tel:${p.phone}` : undefined }), _jsx(QuickAction, { icon: _jsx(Globe, { className: "h-4 w-4" }), label: "Website", href: p.website ?? undefined, external: true })] })] })] })), _jsx(Separator, {}), _jsxs("div", { className: "space-y-3", children: [_jsx(SectionLabel, { children: "Contact" }), _jsx(InfoRow, { icon: _jsx(MapPin, { className: "h-4 w-4" }), value: fullAddress || null }), _jsx(InfoRow, { icon: _jsx(Mail, { className: "h-4 w-4" }), value: p.email, href: p.email ? `mailto:${p.email}` : undefined, hint: p.email_from_contact ? `via ${p.primary_contact_name ?? "contactpersoon"}` : undefined }), _jsx(InfoRow, { icon: _jsx(Phone, { className: "h-4 w-4" }), value: p.phone, href: p.phone ? `tel:${p.phone}` : undefined, hint: p.phone_from_contact ? `via ${p.primary_contact_name ?? "contactpersoon"}` : undefined }), _jsx(InfoRow, { icon: _jsx(Globe, { className: "h-4 w-4" }), value: p.website, href: p.website ?? undefined, external: true }), !p.email && !p.phone && !p.has_contacts && (_jsxs("p", { className: "text-xs text-muted-foreground leading-relaxed", children: ["Voeg e-mail of telefoon toe via ", _jsx("strong", { children: "Bewerken" }), "."] }))] }), p.vat_number && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { className: "space-y-3", children: [_jsx(SectionLabel, { children: "Identificatie" }), _jsx(InfoRow, { icon: _jsx(Hash, { className: "h-4 w-4" }), label: "BTW", value: p.vat_number, mono: true })] })] })), hasTech && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("button", { type: "button", onClick: () => setShowTech((v) => !v), className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [showTech ? _jsx(ChevronDown, { className: "h-3 w-3" }) : _jsx(ChevronRight, { className: "h-3 w-3" }), "Technische gegevens"] }), showTech && (_jsxs("div", { className: "space-y-3", children: [p.rfh_id && _jsx(InfoRow, { icon: _jsx(Hash, { className: "h-4 w-4" }), label: "RFH-relatienr.", value: p.rfh_id, mono: true }), _jsx(InfoRow, { icon: _jsx(Hash, { className: "h-4 w-4" }), label: "GLN", value: p.gln, mono: true }), _jsx(InfoRow, { icon: _jsx(Building2, { className: "h-4 w-4" }), label: "Type", value: p.organization_type })] }))] })), _jsx(Separator, {}), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(SectionLabel, { children: "Notitie" }), editable && !isEditing && (_jsxs("button", { type: "button", onClick: () => setIsEditing(true), className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", "aria-label": "Notitie bewerken", children: [_jsx(Pencil, { className: "h-3 w-3" }), "Bewerken"] }))] }), isEditing ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Textarea, { value: draft, onChange: (e) => setDraft(e.target.value), rows: 4, placeholder: "Voeg een korte notitie over deze klant toe...", disabled: isSaving, className: "text-sm resize-none" }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
129
|
+
setDraft(notes ?? "");
|
|
130
|
+
setIsEditing(false);
|
|
131
|
+
}, disabled: isSaving, children: [_jsx(X, { className: "h-4 w-4 mr-1" }), " Annuleren"] }), _jsx(Button, { type: "button", size: "sm", onClick: handleSave, disabled: isSaving || draft === (notes ?? ""), children: isSaving ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" }), "Opslaan..."] })) : (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4 mr-1" }), " Opslaan"] })) })] })] })) : notes ? (_jsx("p", { className: "text-sm whitespace-pre-wrap text-foreground", children: notes })) : editable ? (_jsxs("button", { type: "button", onClick: () => setIsEditing(true), className: "flex w-full items-center gap-2 rounded-md border border-dashed border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted", children: [_jsx(StickyNote, { className: "h-4 w-4" }), "Notitie toevoegen"] })) : (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(StickyNote, { className: "h-4 w-4" }), "Geen notitie"] }))] })] }) }));
|
|
130
132
|
}
|
|
131
133
|
export function SectionLabel({ children }) {
|
|
132
134
|
return _jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2", children: children });
|
|
@@ -150,7 +152,7 @@ export function KpiStrip({ counts, onSelect, }) {
|
|
|
150
152
|
{ key: "assortment", icon: _jsx(Package, { className: "h-4 w-4" }), label: "Assortiment", value: counts.assortment },
|
|
151
153
|
{ key: "orders", icon: _jsx(ShoppingCart, { className: "h-4 w-4" }), label: "Bestellingen", value: counts.orders },
|
|
152
154
|
];
|
|
153
|
-
return (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "grid grid-cols-2 md:grid-cols-
|
|
155
|
+
return (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "grid grid-cols-2 md:grid-cols-4 divide-y md:divide-y-0 md:divide-x divide-border", children: items.map((it) => (_jsxs("button", { type: "button", onClick: () => onSelect(it.key), className: "flex h-full min-w-0 flex-col items-start justify-between gap-1 p-4 text-left transition-colors hover:bg-muted/50", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx("span", { className: "shrink-0", children: it.icon }), _jsx("span", { className: "truncate", children: it.label })] }), _jsx("span", { className: "text-2xl font-bold", children: it.value })] }, it.key))) }) }) }));
|
|
154
156
|
}
|
|
155
157
|
export function ActivityItem({ entry }) {
|
|
156
158
|
const actionLabels = {
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Route-onafhankelijk: leest `$id` uit welke route de shell ook gebruikt. */
|
|
2
|
+
export declare function KlantDetailRoute({ id: propId }?: {
|
|
3
|
+
id?: string;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
2
5
|
//# sourceMappingURL=KlantDetailRoute.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantDetailRoute.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetailRoute.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantDetailRoute.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetailRoute.tsx"],"names":[],"mappings":"AAGA,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAO,+BAKpE"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useParams } from "@tanstack/react-router";
|
|
3
3
|
import { KlantDetail } from "./KlantDetail";
|
|
4
|
-
|
|
5
|
-
export function KlantDetailRoute() {
|
|
6
|
-
const
|
|
4
|
+
/** Route-onafhankelijk: leest `$id` uit welke route de shell ook gebruikt. */
|
|
5
|
+
export function KlantDetailRoute({ id: propId } = {}) {
|
|
6
|
+
const params = useParams({ strict: false });
|
|
7
|
+
const id = propId ?? params.id;
|
|
8
|
+
if (!id)
|
|
9
|
+
return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Geen klant geselecteerd." });
|
|
7
10
|
return _jsx(KlantDetail, { id: id });
|
|
8
11
|
}
|
|
@@ -12,7 +12,7 @@ export declare const emptyKlantFormValues: KlantFormValues;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function KlantFormulier({ title, description, submitLabel, submitting, defaultValues, onSubmit, onCancel, extra, numberReadOnly, numberHint, }: {
|
|
14
14
|
title: string;
|
|
15
|
-
description
|
|
15
|
+
description?: string;
|
|
16
16
|
submitLabel: string;
|
|
17
17
|
submitting: boolean;
|
|
18
18
|
defaultValues: KlantFormValues;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantFormulier.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantFormulier.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAwB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"KlantFormulier.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantFormulier.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAwB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAcxF,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,eAuBf,CAAC;AAErB;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,WAAW,EACX,WAAW,EACX,UAAU,EACV,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,cAAc,EACd,UAAU,GACX,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,eAAe,CAAC;IAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,+BAulBA"}
|