@flowselections/floriday-klanten-module 1.0.27 → 1.0.29
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.map +1 -1
- package/dist-lib/components/klanten/BewerkKlant.js +2 -1
- package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/DeliveryLocationsTab.js +67 -8
- package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -1
- package/dist-lib/components/klanten/ImportKlanten.js +2 -1
- package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenOverzicht.js +61 -18
- package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts.map +1 -1
- package/dist-lib/components/klanten/LocalDiscountFormDialog.js +2 -1
- package/dist-lib/components/klanten/LocalDiscountsTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/LocalDiscountsTab.js +2 -1
- package/dist-lib/components/klanten/LogisticsStrategyCard.d.ts.map +1 -1
- package/dist-lib/components/klanten/LogisticsStrategyCard.js +2 -1
- package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/NieuweKlant.js +2 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts.map +1 -1
- package/dist-lib/components/klanten/OvernemenTwinfieldKlant.js +2 -1
- package/dist-lib/components/klanten/RouteError.d.ts +6 -0
- package/dist-lib/components/klanten/RouteError.d.ts.map +1 -0
- package/dist-lib/components/klanten/RouteError.js +13 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/customers.functions.js +39 -35
- package/dist-lib/lib/errors.d.ts +7 -0
- package/dist-lib/lib/errors.d.ts.map +1 -0
- package/dist-lib/lib/errors.js +36 -0
- package/dist-lib/lib/external-customers.functions.d.ts +1 -1
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.js +28 -12
- package/dist-lib/lib/persisted-state.d.ts +7 -0
- package/dist-lib/lib/persisted-state.d.ts.map +1 -0
- package/dist-lib/lib/persisted-state.js +92 -0
- package/dist-lib/lib/price-groups-local.functions.d.ts +3 -3
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAmDjD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useNavigate } from "@tanstack/react-router";
|
|
3
4
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
5
|
import { useServerFn } from "@tanstack/react-start";
|
|
@@ -47,7 +48,7 @@ export function BewerkKlant({ id }) {
|
|
|
47
48
|
qc.invalidateQueries({ queryKey: ["twinfield-sync-status", id] });
|
|
48
49
|
navigate({ to: "/klanten/$id", params: { id } });
|
|
49
50
|
},
|
|
50
|
-
onError: (e) => toast.error(e
|
|
51
|
+
onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
|
|
51
52
|
});
|
|
52
53
|
if (isLoading)
|
|
53
54
|
return _jsx("main", { className: "p-6", children: "Laden..." });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeliveryLocationsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/DeliveryLocationsTab.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DeliveryLocationsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/DeliveryLocationsTab.tsx"],"names":[],"mappings":"AAoEA,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,+BAoaA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
5
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
6
|
import { Button, Card, CardContent, Badge, toast, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@flowselections/core";
|
|
@@ -13,6 +14,15 @@ const EMPTY = {
|
|
|
13
14
|
city: "", province: "", country: "Nederland",
|
|
14
15
|
carrier_id: null, carrier_assignment_source: "auto", is_primary: false, self_pickup: false,
|
|
15
16
|
};
|
|
17
|
+
function normalizePostalCode(value, country) {
|
|
18
|
+
const trimmed = value.trim().replace(/\s+/g, " ");
|
|
19
|
+
if (findCountry(country)?.code === "NL") {
|
|
20
|
+
const compact = trimmed.replace(/\s+/g, "").toUpperCase();
|
|
21
|
+
if (/^\d{4}[A-Z]{2}$/.test(compact))
|
|
22
|
+
return `${compact.slice(0, 4)} ${compact.slice(4)}`;
|
|
23
|
+
}
|
|
24
|
+
return trimmed;
|
|
25
|
+
}
|
|
16
26
|
export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, externalKey, }) {
|
|
17
27
|
const qc = useQueryClient();
|
|
18
28
|
const owner = {
|
|
@@ -39,6 +49,37 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
39
49
|
const locations = (list?.locations ?? []);
|
|
40
50
|
const [open, setOpen] = useState(false);
|
|
41
51
|
const [form, setForm] = useState(EMPTY);
|
|
52
|
+
const [touched, setTouched] = useState({});
|
|
53
|
+
const [submitted, setSubmitted] = useState(false);
|
|
54
|
+
const fieldRefs = {
|
|
55
|
+
street: useRef(null),
|
|
56
|
+
house_number: useRef(null),
|
|
57
|
+
postal_code: useRef(null),
|
|
58
|
+
city: useRef(null),
|
|
59
|
+
country: useRef(null),
|
|
60
|
+
province: useRef(null),
|
|
61
|
+
};
|
|
62
|
+
const errors = useMemo(() => {
|
|
63
|
+
const e = {};
|
|
64
|
+
if (form.self_pickup)
|
|
65
|
+
return e;
|
|
66
|
+
if (!form.street.trim())
|
|
67
|
+
e.street = "Straat is verplicht";
|
|
68
|
+
if (!form.house_number.trim())
|
|
69
|
+
e.house_number = "Huisnummer is verplicht";
|
|
70
|
+
if (!form.postal_code.trim())
|
|
71
|
+
e.postal_code = "Postcode is verplicht";
|
|
72
|
+
if (!form.city.trim())
|
|
73
|
+
e.city = "Plaats is verplicht";
|
|
74
|
+
if (!form.country.trim())
|
|
75
|
+
e.country = "Land is verplicht";
|
|
76
|
+
if (!form.province.trim())
|
|
77
|
+
e.province = "Provincie is verplicht";
|
|
78
|
+
return e;
|
|
79
|
+
}, [form]);
|
|
80
|
+
const showError = (field) => (submitted || touched[field]) && errors[field] ? errors[field] : undefined;
|
|
81
|
+
const markTouched = (field) => setTouched((t) => ({ ...t, [field]: true }));
|
|
82
|
+
const req = !form.self_pickup;
|
|
42
83
|
const autoSuggested = useMemo(() => {
|
|
43
84
|
if (form.carrier_assignment_source === "manual")
|
|
44
85
|
return null;
|
|
@@ -50,8 +91,10 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
50
91
|
}
|
|
51
92
|
}, [autoSuggested?.carrier_id, form.carrier_assignment_source]);
|
|
52
93
|
const carrierName = (id) => carriers.find((c) => c.id === id)?.name ?? "—";
|
|
53
|
-
const
|
|
94
|
+
const resetValidation = () => { setTouched({}); setSubmitted(false); };
|
|
95
|
+
const startNew = () => { setForm(EMPTY); resetValidation(); setOpen(true); };
|
|
54
96
|
const startEdit = (loc) => {
|
|
97
|
+
resetValidation();
|
|
55
98
|
setForm({
|
|
56
99
|
id: loc.id,
|
|
57
100
|
name: loc.name ?? "",
|
|
@@ -76,7 +119,7 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
76
119
|
qc.invalidateQueries({ queryKey: ["customer", ownerKey] });
|
|
77
120
|
setOpen(false);
|
|
78
121
|
},
|
|
79
|
-
onError: (e) => toast.error(e
|
|
122
|
+
onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
|
|
80
123
|
});
|
|
81
124
|
const delMut = useMutation({
|
|
82
125
|
mutationFn: (id) => deleteFn({ data: { id } }),
|
|
@@ -86,17 +129,33 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
86
129
|
qc.invalidateQueries({ queryKey: ["customer", ownerKey] });
|
|
87
130
|
},
|
|
88
131
|
});
|
|
89
|
-
|
|
132
|
+
const handleSubmit = () => {
|
|
133
|
+
setSubmitted(true);
|
|
134
|
+
const order = ["street", "house_number", "postal_code", "city", "country", "province"];
|
|
135
|
+
const first = order.find((f) => errors[f]);
|
|
136
|
+
if (first) {
|
|
137
|
+
const el = fieldRefs[first].current;
|
|
138
|
+
el?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
139
|
+
el?.focus();
|
|
140
|
+
toast.error("Vul de verplichte velden in");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
saveMut.mutate();
|
|
144
|
+
};
|
|
145
|
+
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: [_jsxs(Label, { children: ["Straat ", req && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { ref: fieldRefs.street, value: form.street, onChange: (e) => setForm({ ...form, street: e.target.value }), onBlur: () => markTouched("street"), "aria-invalid": !!showError("street"), className: showError("street") ? "border-destructive" : undefined }), showError("street") && _jsx("p", { className: "mt-1 text-xs text-destructive", children: showError("street") })] }), _jsxs("div", { children: [_jsxs(Label, { children: ["Huisnummer ", req && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { ref: fieldRefs.house_number, value: form.house_number, onChange: (e) => setForm({ ...form, house_number: e.target.value }), onBlur: () => markTouched("house_number"), "aria-invalid": !!showError("house_number"), className: showError("house_number") ? "border-destructive" : undefined }), showError("house_number") && _jsx("p", { className: "mt-1 text-xs text-destructive", children: showError("house_number") })] })] }), _jsxs("div", { children: [_jsxs(Label, { children: ["Postcode ", req && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { ref: fieldRefs.postal_code, value: form.postal_code, onChange: (e) => setForm({ ...form, postal_code: e.target.value }), onBlur: () => {
|
|
146
|
+
markTouched("postal_code");
|
|
147
|
+
setForm((f) => ({ ...f, postal_code: normalizePostalCode(f.postal_code, f.country) }));
|
|
148
|
+
}, "aria-invalid": !!showError("postal_code"), className: showError("postal_code") ? "border-destructive" : undefined }), showError("postal_code") && _jsx("p", { className: "mt-1 text-xs text-destructive", children: showError("postal_code") })] }), _jsxs("div", { children: [_jsxs(Label, { children: ["Plaats ", req && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { ref: fieldRefs.city, value: form.city, onChange: (e) => setForm({ ...form, city: e.target.value }), onBlur: () => markTouched("city"), "aria-invalid": !!showError("city"), className: showError("city") ? "border-destructive" : undefined }), showError("city") && _jsx("p", { className: "mt-1 text-xs text-destructive", children: showError("city") })] }), _jsxs("div", { ref: fieldRefs.country, tabIndex: -1, children: [_jsxs(Label, { children: ["Land ", req && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Select, { value: findCountry(form.country)?.name ?? "", onValueChange: (v) => { markTouched("country"); setForm({ ...form, country: v, province: "" }); }, children: [_jsx(SelectTrigger, { className: showError("country") ? "border-destructive" : undefined, children: _jsx(SelectValue, { placeholder: "Kies een land", children: (() => {
|
|
90
149
|
const selectedName = findCountry(form.country)?.name ?? "";
|
|
91
150
|
const flag = countryNameToFlag(selectedName);
|
|
92
151
|
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
152
|
})() }) }), _jsx(SelectContent, { children: EU_COUNTRIES.map((c) => {
|
|
94
153
|
const flag = countryNameToFlag(c.code);
|
|
95
154
|
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: [
|
|
155
|
+
}) })] }), showError("country") && _jsx("p", { className: "mt-1 text-xs text-destructive", children: showError("country") })] }), _jsxs("div", { ref: fieldRefs.province, tabIndex: -1, children: [_jsxs(Label, { children: ["Provincie ", req && _jsx("span", { className: "text-destructive", children: "*" })] }), (() => {
|
|
97
156
|
const provinces = findCountry(form.country)?.provinces ?? [];
|
|
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 }));
|
|
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) => {
|
|
157
|
+
return provinces.length > 0 ? (_jsxs(Select, { value: form.province || "", onValueChange: (v) => { markTouched("province"); setForm({ ...form, province: v }); }, children: [_jsx(SelectTrigger, { className: showError("province") ? "border-destructive" : undefined, 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 }), onBlur: () => markTouched("province"), placeholder: "Kies eerst een land", disabled: !form.country, "aria-invalid": !!showError("province"), className: showError("province") ? "border-destructive" : undefined }));
|
|
158
|
+
})(), showError("province") && _jsx("p", { className: "mt-1 text-xs text-destructive", children: showError("province") })] }), _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) => {
|
|
100
159
|
if (v === "__self__") {
|
|
101
160
|
setForm({ ...form, self_pickup: true, carrier_id: null, carrier_assignment_source: "manual" });
|
|
102
161
|
}
|
|
@@ -107,5 +166,5 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
107
166
|
setForm({ ...form, self_pickup: false, carrier_id: v, carrier_assignment_source: "manual" });
|
|
108
167
|
}
|
|
109
168
|
}, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een vervoerder" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__self__", children: "Eigen vervoer (klant haalt zelf op)" }), _jsx(SelectItem, { value: "__none__", children: "\u2014 Geen vervoerder \u2014" }), carriers.map((c) => (_jsx(SelectItem, { value: c.id, children: c.name }, c.id)))] })] }), form.self_pickup ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "De klant komt zelf de karren ophalen \u2014 er wordt geen vervoerder gekoppeld." })) : form.carrier_assignment_source === "manual" ? (_jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsx("span", { children: "Handmatig overschreven." }), _jsx(Button, { variant: "link", size: "sm", className: "h-auto p-0", onClick: () => setForm({ ...form, carrier_assignment_source: "auto" }), children: "Terug naar automatisch" })] })) : autoSuggested?.carrier_id ? (_jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "Automatisch geselecteerd op basis van ", autoSuggested.matched_on === "city"
|
|
110
|
-
? "plaats" : autoSuggested.matched_on === "province" ? "provincie" : "land", "."] })) : (_jsx("div", { className: "text-xs text-amber-600", children: "Geen vervoerder gevonden voor dit bedieningsgebied \u2014 selecteer er handmatig \u00E9\u00E9n." }))] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setOpen(false), children: "Annuleren" }), _jsx(Button, { onClick:
|
|
169
|
+
? "plaats" : autoSuggested.matched_on === "province" ? "provincie" : "land", "."] })) : (_jsx("div", { className: "text-xs text-amber-600", children: "Geen vervoerder gevonden voor dit bedieningsgebied \u2014 selecteer er handmatig \u00E9\u00E9n." }))] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setOpen(false), children: "Annuleren" }), _jsx(Button, { onClick: handleSubmit, disabled: saveMut.isPending, children: form.id ? "Opslaan" : "Toevoegen" })] })] }) })] }));
|
|
111
170
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"AA+MA,wBAAgB,aAAa,gCAsc5B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useNavigate } from "@tanstack/react-router";
|
|
3
4
|
import { useEffect, useMemo, useState } from "react";
|
|
4
5
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
@@ -235,7 +236,7 @@ export function ImportKlanten() {
|
|
|
235
236
|
queryClient.invalidateQueries({ queryKey: ["customers-external"] });
|
|
236
237
|
}
|
|
237
238
|
},
|
|
238
|
-
onError: (e) => toast.error(e
|
|
239
|
+
onError: (e) => toast.error(friendlyError(e, "Import mislukt")),
|
|
239
240
|
});
|
|
240
241
|
const validCount = useMemo(() => rows.filter((r) => r.data && r.errors.length === 0).length, [rows]);
|
|
241
242
|
const invalidCount = rows.length - validCount;
|
|
@@ -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":"AAoDA,wBAAgB,gBAAgB,gCAkrB/B"}
|
|
@@ -3,6 +3,7 @@ import { Link, 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";
|
|
6
|
+
import { usePersistedState, markReturnToList, consumeReturnToList } from "../../lib/persisted-state";
|
|
6
7
|
import { Button, Input, Card, CardContent, Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Skeleton, } from "@flowselections/core";
|
|
7
8
|
import { Plus, Search, Users, UserCheck, UserX, AlertCircle, X, ArrowUpDown, Mail, MapPin, Upload, ChevronLeft, ChevronRight, Pencil, } from "lucide-react";
|
|
8
9
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
@@ -20,15 +21,38 @@ import { MultiSearchableSelect } from "./MultiSearchableSelect";
|
|
|
20
21
|
export function KlantenOverzicht() {
|
|
21
22
|
const navigate = useNavigate();
|
|
22
23
|
const queryClient = useQueryClient();
|
|
23
|
-
const [search, setSearch] =
|
|
24
|
-
const [selectedClasses, setSelectedClasses] =
|
|
25
|
-
const [selectedCountries, setSelectedCountries] =
|
|
26
|
-
const [selectedChannels, setSelectedChannels] =
|
|
27
|
-
const [selectedPayments, setSelectedPayments] =
|
|
28
|
-
const [view, setView] =
|
|
29
|
-
const [sort, setSort] =
|
|
30
|
-
const [page, setPage] =
|
|
31
|
-
const [pageSize, setPageSize] =
|
|
24
|
+
const [search, setSearch] = usePersistedState("klanten:search", "");
|
|
25
|
+
const [selectedClasses, setSelectedClasses] = usePersistedState("klanten:classes", []);
|
|
26
|
+
const [selectedCountries, setSelectedCountries] = usePersistedState("klanten:countries", []);
|
|
27
|
+
const [selectedChannels, setSelectedChannels] = usePersistedState("klanten:channels", []);
|
|
28
|
+
const [selectedPayments, setSelectedPayments] = usePersistedState("klanten:payments", []);
|
|
29
|
+
const [view, setView] = usePersistedState("klanten:view", "all");
|
|
30
|
+
const [sort, setSort] = usePersistedState("klanten:sort", { col: "company_name", dir: "asc" });
|
|
31
|
+
const [page, setPage] = usePersistedState("klanten:page", 1);
|
|
32
|
+
const [pageSize, setPageSize] = usePersistedState("klanten:pageSize", 25, "local");
|
|
33
|
+
// Zoekveld: de invoer reageert direct, het filteren pas na een korte pauze,
|
|
34
|
+
// zodat typen soepel blijft ook bij een volledig klantenbestand.
|
|
35
|
+
const [searchInput, setSearchInput] = useState(search);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (searchInput === search)
|
|
38
|
+
return;
|
|
39
|
+
const t = setTimeout(() => {
|
|
40
|
+
setSearch(searchInput);
|
|
41
|
+
setPage(1);
|
|
42
|
+
}, 250);
|
|
43
|
+
return () => clearTimeout(t);
|
|
44
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
}, [searchInput]);
|
|
46
|
+
// Paginanummer alleen bewaren binnen de klantenflow; vers binnenkomen start op pagina 1.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!consumeReturnToList())
|
|
49
|
+
setPage(1);
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
}, []);
|
|
52
|
+
const goTo = ((opts) => {
|
|
53
|
+
markReturnToList();
|
|
54
|
+
return navigate(opts);
|
|
55
|
+
});
|
|
32
56
|
const list = useServerFn(listCustomers);
|
|
33
57
|
const listExternal = useServerFn(listExternalCustomers);
|
|
34
58
|
const classesFn = useServerFn(listCustomerClasses);
|
|
@@ -36,16 +60,19 @@ export function KlantenOverzicht() {
|
|
|
36
60
|
const { data: classData } = useQuery({
|
|
37
61
|
queryKey: ["customer-classes"],
|
|
38
62
|
queryFn: () => classesFn({ data: undefined }).then(decodeDeep),
|
|
63
|
+
staleTime: 5 * 60000,
|
|
39
64
|
});
|
|
40
65
|
const { data: localData, isLoading: loadingLocal } = useQuery({
|
|
41
66
|
queryKey: ["customers-local"],
|
|
42
67
|
queryFn: () => list({
|
|
43
68
|
data: { search: "", customer_class: "", country: "", is_active: "all", source: "local" },
|
|
44
69
|
}).then(decodeDeep),
|
|
70
|
+
staleTime: 60000,
|
|
45
71
|
});
|
|
46
72
|
const { data: externalData, isLoading: loadingExternal } = useQuery({
|
|
47
73
|
queryKey: ["customers-external"],
|
|
48
74
|
queryFn: () => listExternal({ data: { status: "all" } }).then(decodeDeep),
|
|
75
|
+
staleTime: 60000,
|
|
49
76
|
});
|
|
50
77
|
const { data: overdueData } = useQuery({
|
|
51
78
|
queryKey: ["overdue-balances"],
|
|
@@ -264,7 +291,11 @@ export function KlantenOverzicht() {
|
|
|
264
291
|
const resetPage = () => setPage(1);
|
|
265
292
|
const activeFilterChips = [];
|
|
266
293
|
if (search)
|
|
267
|
-
activeFilterChips.push({
|
|
294
|
+
activeFilterChips.push({
|
|
295
|
+
key: "s",
|
|
296
|
+
label: `Zoek: "${search}"`,
|
|
297
|
+
clear: () => { setSearchInput(""); setSearch(""); resetPage(); },
|
|
298
|
+
});
|
|
268
299
|
selectedClasses.forEach((cls) => {
|
|
269
300
|
activeFilterChips.push({
|
|
270
301
|
key: `c-${cls}`,
|
|
@@ -297,9 +328,10 @@ export function KlantenOverzicht() {
|
|
|
297
328
|
});
|
|
298
329
|
const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
|
|
299
330
|
const lastUpdated = externalData?.last_fetched_at ?? null;
|
|
300
|
-
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: () =>
|
|
331
|
+
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: "Klanten met minimaal \u00E9\u00E9n bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: "Klanten zonder omzet in de afgelopen 12 maanden.", active: view === "inactive", onClick: () => { setView("inactive"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(AlertCircle, { className: "h-5 w-5" }), tint: "destructive", label: "Achterstallige betalingen", value: totals.overdue, tooltip: (overdueData?.totals
|
|
301
332
|
? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen`
|
|
302
|
-
: "Klanten met openstaande facturen waarvan de vervaldatum is verstreken."), active: view === "overdue", onClick: () => { setView("overdue"); resetPage(); } })] }), _jsxs(Card, { children: [_jsx("div", { className: "border-b px-4 pt-3", children: _jsx(Tabs, { value: view, onValueChange: (v) => { setView(v); resetPage(); }, children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.all ?? 0 })] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.active ?? 0 })] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.inactive ?? 0 })] }), _jsxs(TabsTrigger, { value: "overdue", children: ["Achterstallig ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.overdue ?? 0 })] })] }) }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap p-4 border-b", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value:
|
|
333
|
+
: "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
|
+
setSearchInput("");
|
|
303
335
|
setSearch("");
|
|
304
336
|
setSelectedClasses([]);
|
|
305
337
|
setSelectedCountries([]);
|
|
@@ -307,16 +339,25 @@ export function KlantenOverzicht() {
|
|
|
307
339
|
setSelectedPayments([]);
|
|
308
340
|
setView("all");
|
|
309
341
|
setPage(1);
|
|
310
|
-
}, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 8 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 8, children: _jsx(EmptyState, { onAdd: () =>
|
|
342
|
+
}, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 8 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 8, children: _jsx(EmptyState, { onAdd: () => goTo({ to: "/klanten/nieuw" }), filtered: activeFilterChips.length > 0, onClearFilters: () => {
|
|
343
|
+
setSearchInput("");
|
|
344
|
+
setSearch("");
|
|
345
|
+
setSelectedClasses([]);
|
|
346
|
+
setSelectedCountries([]);
|
|
347
|
+
setSelectedChannels([]);
|
|
348
|
+
setSelectedPayments([]);
|
|
349
|
+
setView("all");
|
|
350
|
+
setPage(1);
|
|
351
|
+
} }) }) })) : pageRows.map((c) => {
|
|
311
352
|
const secondary = c.commercial_name && c.commercial_name !== c.company_name
|
|
312
353
|
? c.commercial_name
|
|
313
354
|
: c.city ?? c.email ?? null;
|
|
314
355
|
return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => !c.group_key && c.id
|
|
315
|
-
?
|
|
316
|
-
:
|
|
356
|
+
? goTo({ to: "/klanten/$id", params: { id: c.id } })
|
|
357
|
+
: goTo({
|
|
317
358
|
to: "/klanten/groep/$groupKey",
|
|
318
359
|
params: { groupKey: c.group_key },
|
|
319
|
-
}), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.overdue && c.overdue.count > 0 && (_jsxs("span", { title: `${c.overdue.count} openstaande factu${c.overdue.count === 1 ? "ur" : "ren"} · oudste vervallen op ${c.overdue.oldest_due}`, className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 text-destructive px-2 py-0.5 text-[11px] font-medium shrink-0", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", formatEuro(c.overdue.total), " achterstallig"] }))] }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: formatCountry(c.country) })] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(StatusPill, { active: c.is_active }), c.id ? (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Klant bewerken", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id: c.id }, onClick: (e) => e.stopPropagation(), children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] }) })) : c.twinfield_code ? (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Twinfield-klant overnemen en bewerken", children: _jsxs(Link, { to: "/klanten/overnemen/$code", params: { code: c.twinfield_code }, onClick: (e) => e.stopPropagation(), children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] }) })) : null] }) })] }, c.key));
|
|
360
|
+
}), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.overdue && c.overdue.count > 0 && (_jsxs("span", { title: `${c.overdue.count} openstaande factu${c.overdue.count === 1 ? "ur" : "ren"} · oudste vervallen op ${c.overdue.oldest_due}`, className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 text-destructive px-2 py-0.5 text-[11px] font-medium shrink-0", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", formatEuro(c.overdue.total), " achterstallig"] }))] }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: formatCountry(c.country) })] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(StatusPill, { active: c.is_active }), c.id ? (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Klant bewerken", children: _jsxs(Link, { to: "/klanten/bewerken/$id", params: { id: c.id }, onClick: (e) => { e.stopPropagation(); markReturnToList(); }, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] }) })) : c.twinfield_code ? (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "h-8 w-8 p-0", title: "Twinfield-klant overnemen en bewerken", children: _jsxs(Link, { to: "/klanten/overnemen/$code", params: { code: c.twinfield_code }, onClick: (e) => { e.stopPropagation(); markReturnToList(); }, children: [_jsx(Pencil, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Bewerken" })] }) })) : null] }) })] }, c.key));
|
|
320
361
|
}) })] }) }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3 border-t text-sm flex-wrap", children: [_jsx("span", { className: "text-muted-foreground", children: isLoading ? "Laden..." : total === 0 ? "0 klanten" : `${startIdx + 1}–${endIdx} van ${total}` }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Per pagina" }), _jsxs(Select, { value: String(pageSize), onValueChange: (v) => { setPageSize(Number(v)); setPage(1); }, children: [_jsx(SelectTrigger, { className: "w-20 h-8", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "25", children: "25" }), _jsx(SelectItem, { value: "50", children: "50" }), _jsx(SelectItem, { value: "100", children: "100" })] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.max(1, p - 1)), disabled: currentPage <= 1, children: [_jsx(ChevronLeft, { className: "h-4 w-4 mr-1" }), " Vorige"] }), _jsxs("span", { className: "text-muted-foreground", children: [currentPage, " / ", totalPages] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.min(totalPages, p + 1)), disabled: currentPage >= totalPages, children: ["Volgende ", _jsx(ChevronRight, { className: "h-4 w-4 ml-1" })] })] })] })] })] }) }));
|
|
321
362
|
}
|
|
322
363
|
function StatCard({ icon, label, value, active, onClick, tooltip, tint = "muted", disabled = false, }) {
|
|
@@ -341,8 +382,10 @@ function StatusPill({ active }) {
|
|
|
341
382
|
? "bg-primary/10 text-primary"
|
|
342
383
|
: "bg-muted text-muted-foreground"}`, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${active ? "bg-primary" : "bg-muted-foreground"}` }), active ? "Actief" : "Inactief"] }));
|
|
343
384
|
}
|
|
344
|
-
function EmptyState({ onAdd }) {
|
|
345
|
-
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-muted flex items-center justify-center", children: _jsx(Users, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Geen klanten
|
|
385
|
+
function EmptyState({ onAdd, filtered = false, onClearFilters, }) {
|
|
386
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-muted flex items-center justify-center", children: _jsx(Users, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: filtered ? "Geen klanten binnen deze filters" : "Nog geen klanten" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: filtered
|
|
387
|
+
? "Verruim of wis je filters om meer klanten te zien."
|
|
388
|
+
: "Voeg je eerste klant toe of importeer een bestand." })] }), _jsxs("div", { className: "flex gap-2 mt-2", children: [filtered && onClearFilters && (_jsxs(Button, { size: "sm", variant: "outline", onClick: onClearFilters, children: [_jsx(X, { className: "h-4 w-4 mr-2" }), " Filters wissen"] })), _jsxs(Button, { size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }));
|
|
346
389
|
}
|
|
347
390
|
function relativeTime(iso) {
|
|
348
391
|
if (!iso)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalDiscountFormDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LocalDiscountFormDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocalDiscountFormDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LocalDiscountFormDialog.tsx"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,iCAAiC,CAAC;AAGzC,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,KAAK,+BAiMzF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useEffect, useState } from "react";
|
|
3
4
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
5
|
import { useServerFn } from "@tanstack/react-start";
|
|
@@ -101,7 +102,7 @@ export function LocalDiscountFormDialog({ open, onOpenChange, customerId, editin
|
|
|
101
102
|
qc.invalidateQueries({ queryKey: ["local-discounts", customerId] });
|
|
102
103
|
onOpenChange(false);
|
|
103
104
|
},
|
|
104
|
-
onError: (err) => toast.error(err
|
|
105
|
+
onError: (err) => toast.error(friendlyError(err, "Opslaan mislukt")),
|
|
105
106
|
});
|
|
106
107
|
const canSave = (source === "agreement" ? !!netPrice : !!discountValue) && !save.isPending;
|
|
107
108
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editing ? "Korting bewerken" : "Nieuwe korting" }) }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Bron" }), _jsxs(Select, { value: source, onValueChange: (v) => setSource(v), disabled: !!editing, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "agreement", children: "Contract (vaste nettoprijs)" }), _jsx(SelectItem, { value: "sales_strategy", children: "Kortingsregel (%/bedrag)" }), _jsx(SelectItem, { value: "price_group", children: "Staffelkorting (o.b.v. hoeveelheid)" })] })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Product" }), _jsxs(Select, { value: productId, onValueChange: setProductId, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een product" }) }), _jsx(SelectContent, { children: products.map((p) => (_jsxs(SelectItem, { value: p.id, children: [p.product, p.sale_price != null ? ` — € ${Number(p.sale_price).toFixed(2)}` : ""] }, p.id))) })] })] }), source === "agreement" ? (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Basisprijs (optioneel)" }), _jsx(Input, { type: "number", step: "0.01", value: basePrice, onChange: (e) => setBasePrice(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Nettoprijs *" }), _jsx(Input, { type: "number", step: "0.01", value: netPrice, onChange: (e) => setNetPrice(e.target.value) })] })] })) : (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { value: discountType, onValueChange: (v) => setDiscountType(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "percentage", children: "Percentage (%)" }), _jsx(SelectItem, { value: "amount", children: "Vast bedrag" })] })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Waarde *" }), _jsx(Input, { type: "number", step: "0.01", value: discountValue, onChange: (e) => setDiscountValue(e.target.value) })] })] })), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Min. hoeveelheid" }), _jsx(Input, { type: "number", step: "1", value: minQuantity, onChange: (e) => setMinQuantity(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Geldig van" }), _jsx(Input, { type: "date", value: validFrom, onChange: (e) => setValidFrom(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Geldig tot" }), _jsx(Input, { type: "date", value: validUntil, onChange: (e) => setValidUntil(e.target.value) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Notities" }), _jsx(Input, { value: notes, onChange: (e) => setNotes(e.target.value) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { disabled: !canSave, onClick: () => save.mutate(), children: save.isPending ? "Opslaan…" : "Opslaan" })] })] }) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalDiscountsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LocalDiscountsTab.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocalDiscountsTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LocalDiscountsTab.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,+BA2JvE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useMemo, useState } from "react";
|
|
3
4
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
5
|
import { useServerFn } from "@tanstack/react-start";
|
|
@@ -71,7 +72,7 @@ export function LocalDiscountsTab({ customerId }) {
|
|
|
71
72
|
toast.success("Korting verwijderd");
|
|
72
73
|
qc.invalidateQueries({ queryKey: ["local-discounts", customerId] });
|
|
73
74
|
},
|
|
74
|
-
onError: (err) => toast.error(err
|
|
75
|
+
onError: (err) => toast.error(friendlyError(err, "Actie mislukt")),
|
|
75
76
|
});
|
|
76
77
|
const filtered = useMemo(() => {
|
|
77
78
|
const rows = data?.rows ?? [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogisticsStrategyCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LogisticsStrategyCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LogisticsStrategyCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/LogisticsStrategyCard.tsx"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,oCAAoC,CAAC;AAI5C,wBAAgB,qBAAqB,CAAC,EACpC,KAAK,EACL,MAAM,EACN,WAAW,GACZ,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,+BAuEA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
4
5
|
import { Card, CardContent, CardHeader, CardTitle, Badge, toast, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
|
|
@@ -25,7 +26,7 @@ export function LogisticsStrategyCard({ scope, refKey, description, }) {
|
|
|
25
26
|
toast.success("Logistieke strategie bijgewerkt");
|
|
26
27
|
qc.invalidateQueries({ queryKey: ["logistics-strategy-binding", scope, refKey] });
|
|
27
28
|
},
|
|
28
|
-
onError: (e) => toast.error(e
|
|
29
|
+
onError: (e) => toast.error(friendlyError(e, "Opslaan mislukt")),
|
|
29
30
|
});
|
|
30
31
|
const strategies = strategiesData?.strategies ?? [];
|
|
31
32
|
const current = bindingData?.binding?.strategy_id ?? null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NieuweKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/NieuweKlant.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NieuweKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/NieuweKlant.tsx"],"names":[],"mappings":"AAeA,wBAAgB,WAAW,gCAgE1B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useNavigate } from "@tanstack/react-router";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
@@ -23,7 +24,7 @@ export function NieuweKlant() {
|
|
|
23
24
|
qc.invalidateQueries({ queryKey: ["customers"] });
|
|
24
25
|
navigate({ to: "/klanten/$id", params: { id: r.customer.id } });
|
|
25
26
|
},
|
|
26
|
-
onError: (e) => toast.error(e
|
|
27
|
+
onError: (e) => toast.error(friendlyError(e, "Klant aanmaken mislukt")),
|
|
27
28
|
});
|
|
28
29
|
const onSubmit = (values) => {
|
|
29
30
|
setPendingData(values);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OvernemenTwinfieldKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/OvernemenTwinfieldKlant.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"OvernemenTwinfieldKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/OvernemenTwinfieldKlant.tsx"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,+BAkGjE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { friendlyError } from "../../lib/errors";
|
|
2
3
|
import { useNavigate } from "@tanstack/react-router";
|
|
3
4
|
import { useEffect } from "react";
|
|
4
5
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
@@ -49,7 +50,7 @@ export function OvernemenTwinfieldKlant({ code }) {
|
|
|
49
50
|
else
|
|
50
51
|
navigate({ to: "/klanten" });
|
|
51
52
|
},
|
|
52
|
-
onError: (e) => toast.error(e
|
|
53
|
+
onError: (e) => toast.error(friendlyError(e, "Overnemen mislukt")),
|
|
53
54
|
});
|
|
54
55
|
if (isLoading || existingId)
|
|
55
56
|
return _jsx("main", { className: "p-6", children: "Laden..." });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Nette foutweergave voor route-loaders en render-fouten binnen Klanten. */
|
|
2
|
+
export declare function RouteError({ error }: {
|
|
3
|
+
error: unknown;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
export declare function RouteNotFound(): import("react").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=RouteError.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteError.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/RouteError.tsx"],"names":[],"mappings":"AAKA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,+BAoBvD;AAED,wBAAgB,aAAa,gCAU5B"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRouter } from "@tanstack/react-router";
|
|
3
|
+
import { Button, Card, CardContent } from "@flowselections/core";
|
|
4
|
+
import { AlertCircle, RefreshCw } from "lucide-react";
|
|
5
|
+
import { friendlyError } from "../../lib/errors";
|
|
6
|
+
/** Nette foutweergave voor route-loaders en render-fouten binnen Klanten. */
|
|
7
|
+
export function RouteError({ error }) {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col items-center justify-center gap-3 py-12 text-center", children: [_jsx("div", { className: "h-12 w-12 rounded-full bg-destructive/10 flex items-center justify-center", children: _jsx(AlertCircle, { className: "h-6 w-6 text-destructive" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Deze pagina kon niet worden geladen" }), _jsx("div", { className: "mt-0.5 text-sm text-muted-foreground", children: friendlyError(error) })] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => router.invalidate(), children: [_jsx(RefreshCw, { className: "mr-2 h-4 w-4" }), " Opnieuw proberen"] })] }) }) }));
|
|
10
|
+
}
|
|
11
|
+
export function RouteNotFound() {
|
|
12
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Card, { children: _jsx(CardContent, { className: "py-12 text-center text-sm text-muted-foreground", children: "Deze pagina bestaat niet (meer)." }) }) }));
|
|
13
|
+
}
|