@flowselections/floriday-klanten-module 1.0.26 → 1.0.28
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/DeliveryLocationsTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/DeliveryLocationsTab.js +65 -7
- package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenOverzicht.js +26 -15
- 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":"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":"AAmEA,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,5 @@
|
|
|
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 { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
5
|
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 +13,15 @@ const EMPTY = {
|
|
|
13
13
|
city: "", province: "", country: "Nederland",
|
|
14
14
|
carrier_id: null, carrier_assignment_source: "auto", is_primary: false, self_pickup: false,
|
|
15
15
|
};
|
|
16
|
+
function normalizePostalCode(value, country) {
|
|
17
|
+
const trimmed = value.trim().replace(/\s+/g, " ");
|
|
18
|
+
if (findCountry(country)?.code === "NL") {
|
|
19
|
+
const compact = trimmed.replace(/\s+/g, "").toUpperCase();
|
|
20
|
+
if (/^\d{4}[A-Z]{2}$/.test(compact))
|
|
21
|
+
return `${compact.slice(0, 4)} ${compact.slice(4)}`;
|
|
22
|
+
}
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
16
25
|
export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, externalKey, }) {
|
|
17
26
|
const qc = useQueryClient();
|
|
18
27
|
const owner = {
|
|
@@ -39,6 +48,37 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
39
48
|
const locations = (list?.locations ?? []);
|
|
40
49
|
const [open, setOpen] = useState(false);
|
|
41
50
|
const [form, setForm] = useState(EMPTY);
|
|
51
|
+
const [touched, setTouched] = useState({});
|
|
52
|
+
const [submitted, setSubmitted] = useState(false);
|
|
53
|
+
const fieldRefs = {
|
|
54
|
+
street: useRef(null),
|
|
55
|
+
house_number: useRef(null),
|
|
56
|
+
postal_code: useRef(null),
|
|
57
|
+
city: useRef(null),
|
|
58
|
+
country: useRef(null),
|
|
59
|
+
province: useRef(null),
|
|
60
|
+
};
|
|
61
|
+
const errors = useMemo(() => {
|
|
62
|
+
const e = {};
|
|
63
|
+
if (form.self_pickup)
|
|
64
|
+
return e;
|
|
65
|
+
if (!form.street.trim())
|
|
66
|
+
e.street = "Straat is verplicht";
|
|
67
|
+
if (!form.house_number.trim())
|
|
68
|
+
e.house_number = "Huisnummer is verplicht";
|
|
69
|
+
if (!form.postal_code.trim())
|
|
70
|
+
e.postal_code = "Postcode is verplicht";
|
|
71
|
+
if (!form.city.trim())
|
|
72
|
+
e.city = "Plaats is verplicht";
|
|
73
|
+
if (!form.country.trim())
|
|
74
|
+
e.country = "Land is verplicht";
|
|
75
|
+
if (!form.province.trim())
|
|
76
|
+
e.province = "Provincie is verplicht";
|
|
77
|
+
return e;
|
|
78
|
+
}, [form]);
|
|
79
|
+
const showError = (field) => (submitted || touched[field]) && errors[field] ? errors[field] : undefined;
|
|
80
|
+
const markTouched = (field) => setTouched((t) => ({ ...t, [field]: true }));
|
|
81
|
+
const req = !form.self_pickup;
|
|
42
82
|
const autoSuggested = useMemo(() => {
|
|
43
83
|
if (form.carrier_assignment_source === "manual")
|
|
44
84
|
return null;
|
|
@@ -50,8 +90,10 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
50
90
|
}
|
|
51
91
|
}, [autoSuggested?.carrier_id, form.carrier_assignment_source]);
|
|
52
92
|
const carrierName = (id) => carriers.find((c) => c.id === id)?.name ?? "—";
|
|
53
|
-
const
|
|
93
|
+
const resetValidation = () => { setTouched({}); setSubmitted(false); };
|
|
94
|
+
const startNew = () => { setForm(EMPTY); resetValidation(); setOpen(true); };
|
|
54
95
|
const startEdit = (loc) => {
|
|
96
|
+
resetValidation();
|
|
55
97
|
setForm({
|
|
56
98
|
id: loc.id,
|
|
57
99
|
name: loc.name ?? "",
|
|
@@ -86,17 +128,33 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
86
128
|
qc.invalidateQueries({ queryKey: ["customer", ownerKey] });
|
|
87
129
|
},
|
|
88
130
|
});
|
|
89
|
-
|
|
131
|
+
const handleSubmit = () => {
|
|
132
|
+
setSubmitted(true);
|
|
133
|
+
const order = ["street", "house_number", "postal_code", "city", "country", "province"];
|
|
134
|
+
const first = order.find((f) => errors[f]);
|
|
135
|
+
if (first) {
|
|
136
|
+
const el = fieldRefs[first].current;
|
|
137
|
+
el?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
138
|
+
el?.focus();
|
|
139
|
+
toast.error("Vul de verplichte velden in");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
saveMut.mutate();
|
|
143
|
+
};
|
|
144
|
+
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: () => {
|
|
145
|
+
markTouched("postal_code");
|
|
146
|
+
setForm((f) => ({ ...f, postal_code: normalizePostalCode(f.postal_code, f.country) }));
|
|
147
|
+
}, "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
148
|
const selectedName = findCountry(form.country)?.name ?? "";
|
|
91
149
|
const flag = countryNameToFlag(selectedName);
|
|
92
150
|
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
151
|
})() }) }), _jsx(SelectContent, { children: EU_COUNTRIES.map((c) => {
|
|
94
152
|
const flag = countryNameToFlag(c.code);
|
|
95
153
|
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: [
|
|
154
|
+
}) })] }), 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
155
|
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) => {
|
|
156
|
+
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 }));
|
|
157
|
+
})(), 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
158
|
if (v === "__self__") {
|
|
101
159
|
setForm({ ...form, self_pickup: true, carrier_id: null, carrier_assignment_source: "manual" });
|
|
102
160
|
}
|
|
@@ -107,5 +165,5 @@ export function DeliveryLocationsTab({ customerId, groupKey, externalProvider, e
|
|
|
107
165
|
setForm({ ...form, self_pickup: false, carrier_id: v, carrier_assignment_source: "manual" });
|
|
108
166
|
}
|
|
109
167
|
}, 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:
|
|
168
|
+
? "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
169
|
}
|
|
@@ -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,gCA+oB/B"}
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
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
|
-
import { useEffect, useMemo, useRef
|
|
5
|
+
import { useEffect, useMemo, useRef } 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,25 @@ 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
|
+
// Paginanummer alleen bewaren binnen de klantenflow; vers binnenkomen start op pagina 1.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!consumeReturnToList())
|
|
36
|
+
setPage(1);
|
|
37
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
|
+
}, []);
|
|
39
|
+
const goTo = ((opts) => {
|
|
40
|
+
markReturnToList();
|
|
41
|
+
return navigate(opts);
|
|
42
|
+
});
|
|
32
43
|
const list = useServerFn(listCustomers);
|
|
33
44
|
const listExternal = useServerFn(listExternalCustomers);
|
|
34
45
|
const classesFn = useServerFn(listCustomerClasses);
|
|
@@ -297,7 +308,7 @@ export function KlantenOverzicht() {
|
|
|
297
308
|
});
|
|
298
309
|
const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
|
|
299
310
|
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: () =>
|
|
311
|
+
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: "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
312
|
? `Klanten met openstaande facturen waarvan de vervaldatum is verstreken.\nTotaal openstaand: ${formatEuro(overdueData.totals.amount)} · ${overdueData.totals.invoices} facturen`
|
|
302
313
|
: "Klanten met openstaande facturen waarvan de vervaldatum is verstreken."), active: view === "overdue", onClick: () => { setView("overdue"); resetPage(); } })] }), _jsxs(Card, { children: [_jsx("div", { className: "border-b px-4 pt-3", children: _jsx(Tabs, { value: view, onValueChange: (v) => { setView(v); resetPage(); }, children: _jsxs(TabsList, { className: "flex-wrap", children: [_jsxs(TabsTrigger, { value: "all", children: ["Alle ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.all ?? 0 })] }), _jsxs(TabsTrigger, { value: "active", children: ["Actief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.active ?? 0 })] }), _jsxs(TabsTrigger, { value: "inactive", children: ["Inactief ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.inactive ?? 0 })] }), _jsxs(TabsTrigger, { value: "overdue", children: ["Achterstallig ", _jsx("span", { className: "ml-1.5 text-xs text-muted-foreground", children: totals.overdue ?? 0 })] })] }) }) }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap p-4 border-b", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op bedrijf, contactpersoon of e-mail...", value: 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: () => {
|
|
303
314
|
setSearch("");
|
|
@@ -307,16 +318,16 @@ export function KlantenOverzicht() {
|
|
|
307
318
|
setSelectedPayments([]);
|
|
308
319
|
setView("all");
|
|
309
320
|
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: () =>
|
|
321
|
+
}, className: "text-xs text-muted-foreground hover:text-foreground underline", children: "Alles wissen" })] })), _jsx(CardContent, { className: "p-0", children: _jsx("div", { className: "max-h-[60vh] overflow-y-auto overflow-x-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children: _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: "Bedrijf", col: "company_name", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Contact" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Klasse", col: "customer_class", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Prijscategorie" }), _jsx(TableHead, { children: "Kanaal" }), _jsx(TableHead, { children: "Betaalvorm" }), _jsx(TableHead, { children: _jsx(SortHeader, { label: "Land", col: "country", sort: sort, onClick: toggleSort }) }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: isLoading ? (Array.from({ length: 6 }).map((_, i) => (_jsx(TableRow, { children: Array.from({ length: 8 }).map((__, j) => (_jsx(TableCell, { className: "py-4", children: _jsx(Skeleton, { className: "h-4 w-full max-w-[160px]" }) }, j))) }, i)))) : pageRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 8, children: _jsx(EmptyState, { onAdd: () => goTo({ to: "/klanten/nieuw" }) }) }) })) : pageRows.map((c) => {
|
|
311
322
|
const secondary = c.commercial_name && c.commercial_name !== c.company_name
|
|
312
323
|
? c.commercial_name
|
|
313
324
|
: c.city ?? c.email ?? null;
|
|
314
325
|
return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => !c.group_key && c.id
|
|
315
|
-
?
|
|
316
|
-
:
|
|
326
|
+
? goTo({ to: "/klanten/$id", params: { id: c.id } })
|
|
327
|
+
: goTo({
|
|
317
328
|
to: "/klanten/groep/$groupKey",
|
|
318
329
|
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));
|
|
330
|
+
}), children: [_jsx(TableCell, { className: "py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(CustomerAvatar, { logoUrl: c.logo_url, name: c.company_name, size: "sm" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.overdue && c.overdue.count > 0 && (_jsxs("span", { title: `${c.overdue.count} openstaande factu${c.overdue.count === 1 ? "ur" : "ren"} · oudste vervallen op ${c.overdue.oldest_due}`, className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 text-destructive px-2 py-0.5 text-[11px] font-medium shrink-0", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", formatEuro(c.overdue.total), " achterstallig"] }))] }), secondary && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: secondary })] })] }) }), _jsx(TableCell, { className: "py-3", children: c.contact_person || c.email ? (_jsxs("div", { className: "min-w-0", children: [c.contact_person && _jsx("div", { className: "text-sm truncate", children: c.contact_person }), c.email && (_jsxs("a", { href: `mailto:${c.email}`, onClick: (e) => e.stopPropagation(), className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:underline truncate", children: [_jsx(Mail, { className: "h-3 w-3" }), " ", c.email] }))] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: c.customer_class ? _jsx(Badge, { variant: "secondary", children: c.customer_class }) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "py-3", children: _jsx(PriceGroupBadges, { groups: c.price_groups }) }), _jsx(TableCell, { className: "py-3", children: _jsx(ChannelBadges, { providers: c.providers }) }), _jsx(TableCell, { className: "py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${paymentToneClasses(c.payment_method.tone)}`, children: c.payment_method.label }) }), _jsx(TableCell, { className: "py-3 text-sm", children: c.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
331
|
}) })] }) }) }), _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
332
|
}
|
|
322
333
|
function StatCard({ icon, label, value, active, onClick, tooltip, tint = "muted", disabled = false, }) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type StorageKind = "session" | "local";
|
|
2
|
+
export declare function usePersistedState<T>(key: string, initial: T, kind?: StorageKind): readonly [T, import("react").Dispatch<import("react").SetStateAction<T>>];
|
|
3
|
+
export declare function markReturnToList(): void;
|
|
4
|
+
export declare function consumeReturnToList(): boolean;
|
|
5
|
+
export declare function clearPersistedState(keys: string[]): void;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=persisted-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persisted-state.d.ts","sourceRoot":"","sources":["../../src/lib/persisted-state.ts"],"names":[],"mappings":"AASA,KAAK,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAmBvC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,GAAE,WAAuB,6EA4B1F;AASD,wBAAgB,gBAAgB,SAO/B;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAW7C;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAUjD"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* State die bewaard blijft tijdens client-side navigatie (en na reload via
|
|
4
|
+
* sessionStorage). SSR-veilig: eerste render gebruikt altijd de default of de
|
|
5
|
+
* in-memory waarde, sessionStorage wordt na hydratie ingelezen.
|
|
6
|
+
*/
|
|
7
|
+
const memoryStore = new Map();
|
|
8
|
+
function storageFor(kind) {
|
|
9
|
+
try {
|
|
10
|
+
return kind === "local" ? localStorage : sessionStorage;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readStored(key, kind) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = storageFor(kind)?.getItem(key);
|
|
19
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function usePersistedState(key, initial, kind = "session") {
|
|
26
|
+
const [value, setValue] = useState(() => memoryStore.has(key) ? memoryStore.get(key) : initial);
|
|
27
|
+
const [hydrated, setHydrated] = useState(false);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (hydrated)
|
|
30
|
+
return;
|
|
31
|
+
setHydrated(true);
|
|
32
|
+
if (memoryStore.has(key))
|
|
33
|
+
return;
|
|
34
|
+
const stored = readStored(key, kind);
|
|
35
|
+
if (stored !== undefined) {
|
|
36
|
+
memoryStore.set(key, stored);
|
|
37
|
+
setValue(stored);
|
|
38
|
+
}
|
|
39
|
+
}, [key, hydrated, kind]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!hydrated)
|
|
42
|
+
return;
|
|
43
|
+
memoryStore.set(key, value);
|
|
44
|
+
try {
|
|
45
|
+
storageFor(kind)?.setItem(key, JSON.stringify(value));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* storage niet beschikbaar */
|
|
49
|
+
}
|
|
50
|
+
}, [key, value, hydrated, kind]);
|
|
51
|
+
return [value, setValue];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Markeer dat de gebruiker binnen de klantenflow blijft, zodat het
|
|
55
|
+
* paginanummer bewaard blijft bij "terug naar klanten" maar wél reset
|
|
56
|
+
* wanneer het overzicht vers wordt geopend vanuit een andere module.
|
|
57
|
+
*/
|
|
58
|
+
const RETURN_KEY = "klanten:returning";
|
|
59
|
+
export function markReturnToList() {
|
|
60
|
+
try {
|
|
61
|
+
sessionStorage.setItem(RETURN_KEY, "1");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* storage niet beschikbaar */
|
|
65
|
+
}
|
|
66
|
+
memoryStore.set(RETURN_KEY, true);
|
|
67
|
+
}
|
|
68
|
+
export function consumeReturnToList() {
|
|
69
|
+
const inMemory = memoryStore.get(RETURN_KEY) === true;
|
|
70
|
+
let stored = false;
|
|
71
|
+
try {
|
|
72
|
+
stored = sessionStorage.getItem(RETURN_KEY) === "1";
|
|
73
|
+
sessionStorage.removeItem(RETURN_KEY);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* storage niet beschikbaar */
|
|
77
|
+
}
|
|
78
|
+
memoryStore.delete(RETURN_KEY);
|
|
79
|
+
return inMemory || stored;
|
|
80
|
+
}
|
|
81
|
+
export function clearPersistedState(keys) {
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
memoryStore.delete(key);
|
|
84
|
+
try {
|
|
85
|
+
sessionStorage.removeItem(key);
|
|
86
|
+
localStorage.removeItem(key);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* storage niet beschikbaar */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -33,9 +33,9 @@ declare const priceGroupInputSchema: z.ZodObject<{
|
|
|
33
33
|
}>>>;
|
|
34
34
|
rounding: z.ZodDefault<z.ZodEnum<{
|
|
35
35
|
none: "none";
|
|
36
|
+
nearest: "nearest";
|
|
36
37
|
up: "up";
|
|
37
38
|
down: "down";
|
|
38
|
-
nearest: "nearest";
|
|
39
39
|
}>>;
|
|
40
40
|
rounding_step: z.ZodOptional<z.ZodNumber>;
|
|
41
41
|
is_active: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -59517,7 +59517,7 @@ export declare const createCustomerPriceGroup: import("@tanstack/start-client-co
|
|
|
59517
59517
|
price_scope: "incl_delivery" | "excl_delivery";
|
|
59518
59518
|
include_stickers: boolean;
|
|
59519
59519
|
unit: string;
|
|
59520
|
-
rounding: "none" | "
|
|
59520
|
+
rounding: "none" | "nearest" | "up" | "down";
|
|
59521
59521
|
is_active: boolean;
|
|
59522
59522
|
calc_type?: "discount" | "surcharge" | null | undefined;
|
|
59523
59523
|
calc_value_type?: "fixed" | "percentage" | null | undefined;
|
|
@@ -79368,7 +79368,7 @@ export declare const updateCustomerPriceGroup: import("@tanstack/start-client-co
|
|
|
79368
79368
|
price_scope: "incl_delivery" | "excl_delivery";
|
|
79369
79369
|
include_stickers: boolean;
|
|
79370
79370
|
unit: string;
|
|
79371
|
-
rounding: "none" | "
|
|
79371
|
+
rounding: "none" | "nearest" | "up" | "down";
|
|
79372
79372
|
is_active: boolean;
|
|
79373
79373
|
calc_type?: "discount" | "surcharge" | null | undefined;
|
|
79374
79374
|
calc_value_type?: "fixed" | "percentage" | null | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/floriday-klanten-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@cloudflare/vite-plugin": "^1.25.5",
|
|
52
52
|
"@eslint/js": "^9.32.0",
|
|
53
53
|
"@flowselections/core": "1.0.16",
|
|
54
|
-
"@lovable.dev/vite-tanstack-config": "2.
|
|
54
|
+
"@lovable.dev/vite-tanstack-config": "2.9.1",
|
|
55
55
|
"@rsbuild/core": "^1.0.0",
|
|
56
56
|
"@tailwindcss/vite": "^4.2.1",
|
|
57
57
|
"@tanstack/react-query": "^5.83.0",
|