@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.
Files changed (34) hide show
  1. package/dist-lib/components/klanten/BewerkKlant.d.ts.map +1 -1
  2. package/dist-lib/components/klanten/BewerkKlant.js +2 -1
  3. package/dist-lib/components/klanten/DeliveryLocationsTab.d.ts.map +1 -1
  4. package/dist-lib/components/klanten/DeliveryLocationsTab.js +67 -8
  5. package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -1
  6. package/dist-lib/components/klanten/ImportKlanten.js +2 -1
  7. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  8. package/dist-lib/components/klanten/KlantenOverzicht.js +61 -18
  9. package/dist-lib/components/klanten/LocalDiscountFormDialog.d.ts.map +1 -1
  10. package/dist-lib/components/klanten/LocalDiscountFormDialog.js +2 -1
  11. package/dist-lib/components/klanten/LocalDiscountsTab.d.ts.map +1 -1
  12. package/dist-lib/components/klanten/LocalDiscountsTab.js +2 -1
  13. package/dist-lib/components/klanten/LogisticsStrategyCard.d.ts.map +1 -1
  14. package/dist-lib/components/klanten/LogisticsStrategyCard.js +2 -1
  15. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
  16. package/dist-lib/components/klanten/NieuweKlant.js +2 -1
  17. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.d.ts.map +1 -1
  18. package/dist-lib/components/klanten/OvernemenTwinfieldKlant.js +2 -1
  19. package/dist-lib/components/klanten/RouteError.d.ts +6 -0
  20. package/dist-lib/components/klanten/RouteError.d.ts.map +1 -0
  21. package/dist-lib/components/klanten/RouteError.js +13 -0
  22. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  23. package/dist-lib/lib/customers.functions.js +39 -35
  24. package/dist-lib/lib/errors.d.ts +7 -0
  25. package/dist-lib/lib/errors.d.ts.map +1 -0
  26. package/dist-lib/lib/errors.js +36 -0
  27. package/dist-lib/lib/external-customers.functions.d.ts +1 -1
  28. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/external-customers.functions.js +28 -12
  30. package/dist-lib/lib/persisted-state.d.ts +7 -0
  31. package/dist-lib/lib/persisted-state.d.ts.map +1 -0
  32. package/dist-lib/lib/persisted-state.js +92 -0
  33. package/dist-lib/lib/price-groups-local.functions.d.ts +3 -3
  34. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAmDjD"}
1
+ {"version":3,"file":"BewerkKlant.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/BewerkKlant.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,WAAW,CAAC,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 instanceof Error ? e.message : "Opslaan mislukt"),
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":"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"}
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 { useEffect, useMemo, useState } from "react";
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 startNew = () => { setForm(EMPTY); setOpen(true); };
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?.message ?? "Opslaan mislukt"),
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
- 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: (() => {
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: [_jsx(Label, { children: "Provincie" }), (() => {
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: () => saveMut.mutate(), disabled: saveMut.isPending, children: form.id ? "Opslaan" : "Toevoegen" })] })] }) })] }));
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":"AA8MA,wBAAgB,aAAa,gCAsc5B"}
1
+ {"version":3,"file":"ImportKlanten.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/ImportKlanten.tsx"],"names":[],"mappings":"AA+MA,wBAAgB,aAAa,gCAsc5B"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { friendlyError } from "../../lib/errors";
2
3
  import { useNavigate } from "@tanstack/react-router";
3
4
  import { useEffect, useMemo, useState } from "react";
4
5
  import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -235,7 +236,7 @@ export function ImportKlanten() {
235
236
  queryClient.invalidateQueries({ queryKey: ["customers-external"] });
236
237
  }
237
238
  },
238
- onError: (e) => toast.error(e instanceof Error ? e.message : "Import mislukt"),
239
+ onError: (e) => toast.error(friendlyError(e, "Import mislukt")),
239
240
  });
240
241
  const validCount = useMemo(() => rows.filter((r) => r.data && r.errors.length === 0).length, [rows]);
241
242
  const invalidCount = rows.length - validCount;
@@ -1 +1 @@
1
- {"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,gBAAgB,gCAmoB/B"}
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] = useState("");
24
- const [selectedClasses, setSelectedClasses] = useState([]);
25
- const [selectedCountries, setSelectedCountries] = useState([]);
26
- const [selectedChannels, setSelectedChannels] = useState([]);
27
- const [selectedPayments, setSelectedPayments] = useState([]);
28
- const [view, setView] = useState("all");
29
- const [sort, setSort] = useState({ col: "company_name", dir: "asc" });
30
- const [page, setPage] = useState(1);
31
- const [pageSize, setPageSize] = useState(25);
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({ key: "s", label: `Zoek: "${search}"`, clear: () => { setSearch(""); resetPage(); } });
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: () => navigate({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => navigate({ 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
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: search, onChange: (e) => { setSearch(e.target.value); resetPage(); }, className: "pl-9" })] }), _jsx(MultiSearchableSelect, { values: selectedClasses, onValuesChange: (v) => { setSelectedClasses(v); resetPage(); }, options: classOptions, placeholder: "Alle klassen", triggerClassName: "w-40" }), _jsx(MultiSearchableSelect, { values: selectedCountries, onValuesChange: (v) => { setSelectedCountries(v); resetPage(); }, options: countryOptions, placeholder: "Alle landen", triggerClassName: "w-36" }), _jsx(MultiSearchableSelect, { values: selectedChannels, onValuesChange: (v) => { setSelectedChannels(v); resetPage(); }, options: channelSelectOptions, placeholder: "Alle kanalen", triggerClassName: "w-40" }), _jsx(MultiSearchableSelect, { values: selectedPayments, onValuesChange: (v) => { setSelectedPayments(v); resetPage(); }, options: paymentSelectOptions, placeholder: "Alle betaalvormen", triggerClassName: "w-40" })] }), activeFilterChips.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap px-4 py-2 border-b bg-muted/30", children: [activeFilterChips.map((chip) => (_jsxs("button", { type: "button", onClick: chip.clear, className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs hover:bg-muted", children: [chip.label, " ", _jsx(X, { className: "h-3 w-3" })] }, chip.key))), _jsx("button", { type: "button", onClick: () => {
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: () => navigate({ to: "/klanten/nieuw" }) }) }) })) : pageRows.map((c) => {
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
- ? navigate({ to: "/klanten/$id", params: { id: c.id } })
316
- : navigate({
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 gevonden" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Pas je filters aan of voeg handmatig een klant toe." })] }), _jsx("div", { className: "flex gap-2 mt-2", children: _jsxs(Button, { size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] }) })] }));
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":"AAQA,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
+ {"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.message),
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":"AAkDA,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,+BA2JvE"}
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.message),
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":"AAOA,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
+ {"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 instanceof Error ? e.message : "Opslaan mislukt"),
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":"AAcA,wBAAgB,WAAW,gCAgE1B"}
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 instanceof Error ? e.message : "Fout"),
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":"AAuBA;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,+BAkGjE"}
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 instanceof Error ? e.message : "Overnemen mislukt"),
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
+ }