@flowselections/klanten-installatie-techniek-planner 0.0.0-stage → 1.0.19
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/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/KlantenDetailPage.d.ts +2 -0
- package/dist-lib/components/KlantenDetailPage.d.ts.map +1 -0
- package/dist-lib/components/KlantenDetailPage.js +30 -0
- package/dist-lib/components/KlantenListPage.d.ts +6 -0
- package/dist-lib/components/KlantenListPage.d.ts.map +1 -0
- package/dist-lib/components/KlantenListPage.js +105 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
- package/dist-lib/components/TemplateModuleLogo.js +4 -0
- package/dist-lib/components/TemplatePage.d.ts +2 -0
- package/dist-lib/components/TemplatePage.d.ts.map +1 -0
- package/dist-lib/components/TemplatePage.js +4 -0
- package/dist-lib/components/WerkbonDetailPage.d.ts +2 -0
- package/dist-lib/components/WerkbonDetailPage.d.ts.map +1 -0
- package/dist-lib/components/WerkbonDetailPage.js +96 -0
- package/dist-lib/components/klanten/KlantDialog.d.ts +10 -0
- package/dist-lib/components/klanten/KlantDialog.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantDialog.js +37 -0
- package/dist-lib/components/klanten/KlantFormSections.d.ts +45 -0
- package/dist-lib/components/klanten/KlantFormSections.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantFormSections.js +45 -0
- package/dist-lib/components/klanten/KlantGeschiedenisCard.d.ts +6 -0
- package/dist-lib/components/klanten/KlantGeschiedenisCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantGeschiedenisCard.js +105 -0
- package/dist-lib/components/klanten/KlantKoppelingCard.d.ts +8 -0
- package/dist-lib/components/klanten/KlantKoppelingCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantKoppelingCard.js +69 -0
- package/dist-lib/components/klanten/KlantNotitiesCard.d.ts +6 -0
- package/dist-lib/components/klanten/KlantNotitiesCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantNotitiesCard.js +119 -0
- package/dist-lib/components/klanten/KlantStatsCard.d.ts +4 -0
- package/dist-lib/components/klanten/KlantStatsCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantStatsCard.js +21 -0
- package/dist-lib/components/klanten/KlantenPodium.d.ts +8 -0
- package/dist-lib/components/klanten/KlantenPodium.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantenPodium.js +16 -0
- package/dist-lib/components/klanten/useKlantForm.d.ts +42 -0
- package/dist-lib/components/klanten/useKlantForm.d.ts.map +1 -0
- package/dist-lib/components/klanten/useKlantForm.js +240 -0
- package/dist-lib/index.d.ts +8 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +21 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +13 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
- package/dist-lib/integrations/supabase/client.d.ts +4 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +14 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +4 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +30 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.js +99 -0
- package/dist-lib/integrations/supabase/types.d.ts +36447 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +94 -0
- package/dist-lib/lib/itkClient.d.ts +76 -0
- package/dist-lib/lib/itkClient.d.ts.map +1 -0
- package/dist-lib/lib/itkClient.js +5 -0
- package/dist-lib/lib/klantRules.d.ts +27 -0
- package/dist-lib/lib/klantRules.d.ts.map +1 -0
- package/dist-lib/lib/klantRules.js +31 -0
- package/dist-lib/lib/klantRules.test.d.ts +2 -0
- package/dist-lib/lib/klantRules.test.d.ts.map +1 -0
- package/dist-lib/lib/klantRules.test.js +50 -0
- package/dist-lib/lib/klantStatsClient.d.ts +19 -0
- package/dist-lib/lib/klantStatsClient.d.ts.map +1 -0
- package/dist-lib/lib/klantStatsClient.js +44 -0
- package/dist-lib/lib/planningClient.d.ts +22 -0
- package/dist-lib/lib/planningClient.d.ts.map +1 -0
- package/dist-lib/lib/planningClient.js +94 -0
- package/dist-lib/lib/statusConfig.d.ts +7 -0
- package/dist-lib/lib/statusConfig.d.ts.map +1 -0
- package/dist-lib/lib/statusConfig.js +8 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +226 -4
- package/public/logo.png +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Forceert Tailwind classes die uit @flowselections/core komen maar niet
|
|
2
|
+
// voorkomen in de eigen broncode van deze module.
|
|
3
|
+
// Zonder dit bestand worden deze classes weggeoptimaliseerd op de
|
|
4
|
+
// Lovable productie build.
|
|
5
|
+
const _safelist = [
|
|
6
|
+
// Layout — Sidebar en shell structuur
|
|
7
|
+
'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
|
|
8
|
+
'items-start', 'items-center', 'justify-between', 'border-t',
|
|
9
|
+
'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
|
|
10
|
+
// Grid — InstellingenPage
|
|
11
|
+
'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
|
|
12
|
+
// Toggle/Switch — aan/uit zichtbaarheid
|
|
13
|
+
'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
|
|
14
|
+
];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantenDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/KlantenDetailPage.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,iBAAiB,gCAkLhC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigate, useParams, Link } from "@tanstack/react-router";
|
|
3
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Separator, } from "@flowselections/core";
|
|
4
|
+
import { ArrowLeft, Trash2, Save, MapPin } from "lucide-react";
|
|
5
|
+
import { useKlantForm } from "./klanten/useKlantForm";
|
|
6
|
+
import { SegmentToggle, AlgemeneGegevensFields, ContactenFields, LocatiesFields, DynamicFieldsGrid, } from "./klanten/KlantFormSections";
|
|
7
|
+
import { KlantGeschiedenisCard } from "./klanten/KlantGeschiedenisCard";
|
|
8
|
+
import { KlantNotitiesCard } from "./klanten/KlantNotitiesCard";
|
|
9
|
+
import { KlantStatsCard } from "./klanten/KlantStatsCard";
|
|
10
|
+
export function KlantenDetailPage() {
|
|
11
|
+
const { id } = useParams({ from: "/_authenticated/installatietechniek-klanten-planner/$id" });
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, reload, } = useKlantForm({
|
|
14
|
+
customerId: id,
|
|
15
|
+
onSaved: (newId) => {
|
|
16
|
+
if (newId !== id) {
|
|
17
|
+
navigate({ to: "/installatietechniek-klanten-planner/$id", params: { id: newId } });
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
void reload();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
onDeleted: () => navigate({ to: "/installatietechniek-klanten-planner" }),
|
|
24
|
+
onNotFound: () => navigate({ to: "/installatietechniek-klanten-planner" }),
|
|
25
|
+
});
|
|
26
|
+
if (loading) {
|
|
27
|
+
return (_jsx("main", { className: "p-6", children: _jsx("p", { className: "text-muted-foreground", children: "Laden\u2026" }) }));
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs("main", { className: "p-6 space-y-6 max-w-5xl", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Link, { to: "/installatietechniek-klanten-planner", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-2", children: [_jsx(ArrowLeft, { className: "w-4 h-4" }), "Terug"] }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground", children: isNew ? "Nieuwe klant" : form.name || "Klant" }), customerNumber && (_jsx("span", { className: "font-mono text-xs text-muted-foreground bg-muted px-2 py-1 rounded", children: customerNumber }))] }), _jsxs("div", { className: "flex gap-2", children: [!isNew && (_jsxs(Button, { variant: "outline", onClick: remove, className: "gap-2 text-destructive", children: [_jsx(Trash2, { className: "w-4 h-4" }), "Verwijderen"] })), _jsxs(Button, { onClick: save, disabled: saving, className: "gap-2", children: [_jsx(Save, { className: "w-4 h-4" }), saving ? "Opslaan…" : "Opslaan"] })] })] }), !isNew && (_jsxs("nav", { className: "flex flex-wrap gap-3 text-xs text-muted-foreground border-b pb-3", children: [_jsx("a", { href: "#gegevens", className: "hover:text-foreground transition-colors", children: "Gegevens" }), form.segment === "Business" && (_jsx("a", { href: "#contactpersonen", className: "hover:text-foreground transition-colors", children: "Contactpersonen" })), _jsx("a", { href: "#werklocaties", className: "hover:text-foreground transition-colors", children: "Werklocaties" }), fields.length > 0 && (_jsx("a", { href: "#aanvullend", className: "hover:text-foreground transition-colors", children: "Aanvullend" })), _jsx("a", { href: "#werkbonnen", className: "hover:text-foreground transition-colors", children: "Werkbonnen" }), _jsx("a", { href: "#notities", className: "hover:text-foreground transition-colors", children: "Notities" })] })), !isNew && _jsx(KlantStatsCard, { customerId: id }), _jsxs(Card, { id: "gegevens", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Segment" }) }), _jsx(CardContent, { children: _jsx(SegmentToggle, { segment: form.segment, onChange: (s) => setForm((f) => ({ ...f, segment: s })) }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Algemene gegevens" }) }), _jsx(CardContent, { children: _jsx(AlgemeneGegevensFields, { form: form, setForm: setForm, isNew: isNew }) })] }), form.segment === "Business" && (_jsxs(Card, { id: "contactpersonen", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Contactpersonen" }) }), _jsx(CardContent, { children: _jsx(ContactenFields, { contacts: contacts, addContact: addContact, updateContact: updateContact, removeContact: removeContact }) })] })), _jsxs(Card, { id: "werklocaties", children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(MapPin, { className: "w-4 h-4" }), "Werklocaties"] }) }), _jsx(CardContent, { children: _jsx(LocatiesFields, { locations: locations, addLocation: addLocation, updateLocation: updateLocation, removeLocation: removeLocation }) })] }), fields.length > 0 && (_jsxs(Card, { id: "aanvullend", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Aanvullende velden" }) }), _jsx(CardContent, { children: _jsx(DynamicFieldsGrid, { fields: fields, values: values, setValues: setValues }) })] })), !isNew && (_jsxs(_Fragment, { children: [_jsx(KlantGeschiedenisCard, { customerId: id }), _jsx(KlantNotitiesCard, { customerId: id })] })), _jsx(Separator, {}), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { onClick: save, disabled: saving, className: "gap-2", children: [_jsx(Save, { className: "w-4 h-4" }), saving ? "Opslaan…" : "Opslaan"] }) })] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CustomerSegment } from "../lib/itkClient";
|
|
2
|
+
export declare function KlantenListPage(): import("react").JSX.Element;
|
|
3
|
+
export declare function SegmentBadge({ segment }: {
|
|
4
|
+
segment: CustomerSegment;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=KlantenListPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantenListPage.d.ts","sourceRoot":"","sources":["../../src/components/KlantenListPage.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAU3E,wBAAgB,eAAe,gCAsO9B;AAED,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,+BAkBrE"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Button, Card, CardContent, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Badge, toast, } from "@flowselections/core";
|
|
4
|
+
import { Plus, Search, Building2, User } from "lucide-react";
|
|
5
|
+
import { db } from "../lib/itkClient";
|
|
6
|
+
import { fetchKlantStats, fetchTopKlanten, formatEuro } from "../lib/klantStatsClient";
|
|
7
|
+
import { KlantDialog } from "./klanten/KlantDialog";
|
|
8
|
+
import { KlantenPodium } from "./klanten/KlantenPodium";
|
|
9
|
+
const PAGE_SIZE = 50;
|
|
10
|
+
export function KlantenListPage() {
|
|
11
|
+
const [customers, setCustomers] = useState([]);
|
|
12
|
+
const [total, setTotal] = useState(0);
|
|
13
|
+
const [page, setPage] = useState(0);
|
|
14
|
+
const [stats, setStats] = useState({});
|
|
15
|
+
const [statsFailed, setStatsFailed] = useState(false);
|
|
16
|
+
const [top, setTop] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [loadError, setLoadError] = useState(false);
|
|
19
|
+
const [search, setSearch] = useState("");
|
|
20
|
+
const [debounced, setDebounced] = useState("");
|
|
21
|
+
const [segmentFilter, setSegmentFilter] = useState("all");
|
|
22
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
23
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
24
|
+
const reqId = useRef(0);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const t = setTimeout(() => setDebounced(search.trim()), 300);
|
|
27
|
+
return () => clearTimeout(t);
|
|
28
|
+
}, [search]);
|
|
29
|
+
useEffect(() => setPage(0), [debounced, segmentFilter]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
void load();
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [page, debounced, segmentFilter]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
void loadTop();
|
|
36
|
+
}, []);
|
|
37
|
+
const openCustomer = (id) => {
|
|
38
|
+
setSelectedId(id);
|
|
39
|
+
setDialogOpen(true);
|
|
40
|
+
};
|
|
41
|
+
const loadTop = async () => {
|
|
42
|
+
try {
|
|
43
|
+
setTop(await fetchTopKlanten(3));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
setTop([]);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const load = async () => {
|
|
50
|
+
const my = ++reqId.current;
|
|
51
|
+
setLoading(true);
|
|
52
|
+
setLoadError(false);
|
|
53
|
+
let query = db
|
|
54
|
+
.from("itk_customers")
|
|
55
|
+
.select("*", { count: "exact" })
|
|
56
|
+
.order("created_at", { ascending: false })
|
|
57
|
+
.range(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE - 1);
|
|
58
|
+
if (segmentFilter !== "all")
|
|
59
|
+
query = query.eq("segment", segmentFilter);
|
|
60
|
+
if (debounced) {
|
|
61
|
+
const q = debounced.replace(/[%_,()*\\]/g, " ").trim();
|
|
62
|
+
if (q) {
|
|
63
|
+
const p = `%${q}%`;
|
|
64
|
+
query = query.or(`name.ilike.${p},customer_number.ilike.${p},general_email.ilike.${p},billing_city.ilike.${p}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const { data, error, count } = await query;
|
|
68
|
+
if (my !== reqId.current)
|
|
69
|
+
return;
|
|
70
|
+
if (error) {
|
|
71
|
+
setLoadError(true);
|
|
72
|
+
setCustomers([]);
|
|
73
|
+
toast.error("Klanten konden niet geladen worden");
|
|
74
|
+
setLoading(false);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const list = Array.isArray(data) ? data : [];
|
|
78
|
+
setCustomers(list);
|
|
79
|
+
setTotal(typeof count === "number" ? count : list.length);
|
|
80
|
+
setLoading(false);
|
|
81
|
+
try {
|
|
82
|
+
const s = await fetchKlantStats(list.map((c) => c.id));
|
|
83
|
+
if (my !== reqId.current)
|
|
84
|
+
return;
|
|
85
|
+
setStats(s);
|
|
86
|
+
setStatsFailed(false);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
if (my !== reqId.current)
|
|
90
|
+
return;
|
|
91
|
+
setStats({});
|
|
92
|
+
setStatsFailed(true);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const filtered = customers;
|
|
96
|
+
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
97
|
+
const statCell = (id, pick) => stats[id] ? pick(stats[id]) : statsFailed ? "n.b." : "—";
|
|
98
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-3 sm:flex sm:flex-wrap sm:items-center sm:gap-6", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "truncate text-2xl font-bold text-foreground", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Beheer al je zakelijke en particuliere klanten" })] }), top.length > 0 && (_jsx("div", { className: "col-span-2 justify-self-start sm:col-span-1 sm:self-center sm:justify-self-auto", children: _jsx(KlantenPodium, { items: top, onSelect: openCustomer }) })), _jsxs(Button, { className: "col-start-2 row-start-1 shrink-0 justify-self-end gap-2 sm:col-start-auto sm:row-start-auto sm:ml-auto sm:justify-self-auto", onClick: () => openCustomer(null), children: [_jsx(Plus, { className: "w-4 h-4" }), "Nieuwe klant"] })] }), _jsx(Card, { children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex gap-3 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px]", children: [_jsx(Search, { className: "w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op klantnr, naam, e-mail of plaats\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsxs(Select, { value: segmentFilter, onValueChange: (v) => setSegmentFilter(v), children: [_jsx(SelectTrigger, { className: "w-[180px]", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle segmenten" }), _jsx(SelectItem, { value: "Business", children: "Zakelijk" }), _jsx(SelectItem, { value: "Private", children: "Particulier" })] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[130px]", children: "Klantnr." }), _jsx(TableHead, { className: "w-[30%]", children: "Naam" }), _jsx(TableHead, { children: "Segment" }), _jsx(TableHead, { children: "E-mail" }), _jsx(TableHead, { children: "Telefoon" }), _jsx(TableHead, { children: "Plaats" }), _jsx(TableHead, { className: "text-right", children: "Omzet" }), _jsx(TableHead, { className: "text-right", children: "Klussen" })] }) }), _jsx(TableBody, { children: loading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 8, className: "text-center text-muted-foreground py-8", children: "Laden\u2026" }) })) : filtered.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 8, className: "text-center text-muted-foreground py-8", children: loadError ? "Klanten konden niet geladen worden" : "Geen klanten gevonden" }) })) : (filtered.map((c) => (_jsxs(TableRow, { className: "hover:bg-muted/40 cursor-pointer", onClick: () => openCustomer(c.id), children: [_jsx(TableCell, { className: "font-mono text-xs text-muted-foreground", children: c.customer_number ?? "—" }), _jsx(TableCell, { className: "font-medium text-foreground", children: c.name }), _jsx(TableCell, { children: _jsx(SegmentBadge, { segment: c.segment }) }), _jsx(TableCell, { className: "text-sm", children: c.general_email ?? "—" }), _jsx(TableCell, { className: "text-sm", children: c.general_phone ?? "—" }), _jsx(TableCell, { className: "text-sm", children: c.billing_city ?? "—" }), _jsx(TableCell, { className: "text-sm text-right tabular-nums", children: statCell(c.id, (s) => formatEuro(s.omzetCents)) }), _jsx(TableCell, { className: "text-sm text-right tabular-nums", children: statCell(c.id, (s) => s.klussen) })] }, c.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 text-sm text-muted-foreground", children: [_jsxs("span", { children: [total, " ", total === 1 ? "klant" : "klanten", statsFailed && " · omzet/klussen niet beschikbaar"] }), pageCount > 1 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: page === 0 || loading, onClick: () => setPage((p) => p - 1), children: "Vorige" }), _jsxs("span", { className: "tabular-nums", children: [page + 1, " / ", pageCount] }), _jsx(Button, { variant: "outline", size: "sm", disabled: page + 1 >= pageCount || loading, onClick: () => setPage((p) => p + 1), children: "Volgende" })] }))] })] }) }), _jsx(KlantDialog, { open: dialogOpen, onOpenChange: setDialogOpen, customerId: selectedId, onChanged: () => { void load(); void loadTop(); } })] }));
|
|
99
|
+
}
|
|
100
|
+
export function SegmentBadge({ segment }) {
|
|
101
|
+
if (segment === "Business") {
|
|
102
|
+
return (_jsxs(Badge, { className: "bg-primary text-primary-foreground gap-1 border-transparent", children: [_jsx(Building2, { className: "w-3 h-3" }), "Zakelijk"] }));
|
|
103
|
+
}
|
|
104
|
+
return (_jsxs(Badge, { className: "gap-1 border-transparent text-white", style: { backgroundColor: "hsl(160 84% 39%)" }, children: [_jsx(User, { className: "w-3 h-3" }), "Particulier"] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateModuleLogo.d.ts","sourceRoot":"","sources":["../../src/components/TemplateModuleLogo.tsx"],"names":[],"mappings":"AAQA,UAAU,uBAAuB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE,uBAAuB,+BAwBxE"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function TemplateModuleLogo({ className }) {
|
|
3
|
+
return (_jsxs("svg", { viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className, "aria-hidden": "true", children: [_jsx("rect", { width: "40", height: "40", rx: "8", fill: "currentColor", opacity: "0.1" }), _jsx("text", { x: "50%", y: "50%", dominantBaseline: "middle", textAnchor: "middle", fontSize: "18", fontWeight: "600", fill: "currentColor", children: "M" })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplatePage.d.ts","sourceRoot":"","sources":["../../src/components/TemplatePage.tsx"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,gCAS3B"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function TemplatePage() {
|
|
3
|
+
return (_jsxs("div", { className: "p-6", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Module pagina" }), _jsx("p", { className: "text-muted-foreground mt-2", children: "Vervang dit component met de pagina's van jouw module." })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WerkbonDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/WerkbonDetailPage.tsx"],"names":[],"mappings":"AAkGA,wBAAgB,iBAAiB,gCAoMhC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useParams, Link } from "@tanstack/react-router";
|
|
4
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, } from "@flowselections/core";
|
|
5
|
+
import { ArrowLeft, Wrench, MapPin, CalendarClock, FileText, Euro, Info, } from "lucide-react";
|
|
6
|
+
import { db } from "../lib/itkClient";
|
|
7
|
+
const STATUS_LABEL = {
|
|
8
|
+
unassigned: "Niet toegewezen",
|
|
9
|
+
planned_hidden: "Ingepland",
|
|
10
|
+
planned_visible: "Ingepland",
|
|
11
|
+
in_progress: "In uitvoering",
|
|
12
|
+
completed: "Afgerond",
|
|
13
|
+
invoicing: "Facturatie",
|
|
14
|
+
};
|
|
15
|
+
const STATUS_STYLE = {
|
|
16
|
+
unassigned: "bg-muted text-muted-foreground",
|
|
17
|
+
planned_hidden: "bg-blue-500/15 text-blue-400",
|
|
18
|
+
planned_visible: "bg-blue-500/15 text-blue-400",
|
|
19
|
+
in_progress: "bg-amber-500/15 text-amber-400",
|
|
20
|
+
completed: "bg-emerald-500/15 text-emerald-400",
|
|
21
|
+
invoicing: "bg-purple-500/15 text-purple-400",
|
|
22
|
+
};
|
|
23
|
+
function formatDate(iso) {
|
|
24
|
+
if (!iso)
|
|
25
|
+
return "—";
|
|
26
|
+
try {
|
|
27
|
+
return new Date(iso).toLocaleString("nl-NL", {
|
|
28
|
+
day: "2-digit",
|
|
29
|
+
month: "short",
|
|
30
|
+
year: "numeric",
|
|
31
|
+
hour: "2-digit",
|
|
32
|
+
minute: "2-digit",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return iso;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function formatEuro(cents) {
|
|
40
|
+
if (cents == null)
|
|
41
|
+
return "—";
|
|
42
|
+
return new Intl.NumberFormat("nl-NL", {
|
|
43
|
+
style: "currency",
|
|
44
|
+
currency: "EUR",
|
|
45
|
+
}).format(cents / 100);
|
|
46
|
+
}
|
|
47
|
+
function Veld({ label, value }) {
|
|
48
|
+
return (_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: label }), _jsx("dd", { className: "text-sm text-foreground mt-0.5 break-words", children: value && value.trim() !== "" ? value : "—" })] }));
|
|
49
|
+
}
|
|
50
|
+
export function WerkbonDetailPage() {
|
|
51
|
+
const { id } = useParams({
|
|
52
|
+
from: "/_authenticated/installatietechniek-klanten-planner/werkbonnen/$id",
|
|
53
|
+
});
|
|
54
|
+
const [werkbon, setWerkbon] = useState(null);
|
|
55
|
+
const [loading, setLoading] = useState(true);
|
|
56
|
+
const [notFound, setNotFound] = useState(false);
|
|
57
|
+
const [loadError, setLoadError] = useState(false);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
void (async () => {
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setNotFound(false);
|
|
62
|
+
setLoadError(false);
|
|
63
|
+
const { data, error } = await db
|
|
64
|
+
.from("werkbonnen")
|
|
65
|
+
.select("id, project_number, status, customer_id, customer_name, work_address, room_number, complaint_description, technician_task_description, technician_notes, internal_notes, scheduled_start_time, scheduled_end_time, deadline_date, estimated_duration_minutes, estimated_days, budget_mandate_cents, extra_costs_cents, first_time_fix, afgerond_op, externe_referentie, created_at, updated_at")
|
|
66
|
+
.eq("id", id)
|
|
67
|
+
.maybeSingle();
|
|
68
|
+
if (error) {
|
|
69
|
+
setLoadError(true);
|
|
70
|
+
}
|
|
71
|
+
else if (!data) {
|
|
72
|
+
setNotFound(true);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
setWerkbon(data);
|
|
76
|
+
}
|
|
77
|
+
setLoading(false);
|
|
78
|
+
})();
|
|
79
|
+
}, [id]);
|
|
80
|
+
if (loading) {
|
|
81
|
+
return (_jsx("main", { className: "p-6", children: _jsx("p", { className: "text-muted-foreground", children: "Laden\u2026" }) }));
|
|
82
|
+
}
|
|
83
|
+
if (notFound || loadError || !werkbon) {
|
|
84
|
+
return (_jsxs("main", { className: "p-6 max-w-3xl space-y-4", children: [_jsx(Link, { to: "/installatietechniek-klanten-planner", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-2", children: [_jsx(ArrowLeft, { className: "w-4 h-4" }), "Terug naar overzicht"] }) }), _jsx(Card, { children: _jsxs(CardContent, { className: "py-10 text-center space-y-2", children: [_jsx(Wrench, { className: "w-8 h-8 mx-auto text-muted-foreground" }), _jsx("h1", { className: "text-lg font-semibold text-foreground", children: notFound ? "Werkbon niet gevonden" : "Werkbon kan niet geladen worden" }), _jsx("p", { className: "text-sm text-muted-foreground", children: notFound
|
|
85
|
+
? "Deze werkbon bestaat niet (meer) of je hebt er geen toegang toe."
|
|
86
|
+
: "Er ging iets mis bij het ophalen van de werkbon. Probeer het later opnieuw." })] }) })] }));
|
|
87
|
+
}
|
|
88
|
+
return (_jsxs("main", { className: "p-4 sm:p-6 space-y-6 max-w-5xl", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Link, { to: "/installatietechniek-klanten-planner", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-2", children: [_jsx(ArrowLeft, { className: "w-4 h-4" }), "Terug"] }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground", children: werkbon.project_number ?? "Werkbon" }), _jsx("span", { className: "text-xs px-2 py-0.5 rounded-full font-medium " +
|
|
89
|
+
(STATUS_STYLE[werkbon.status] ?? "bg-muted text-muted-foreground"), children: STATUS_LABEL[werkbon.status] ?? werkbon.status })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(Info, { className: "w-4 h-4" }), "Algemeen"] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [_jsx(Veld, { label: "Werkbonnummer", value: werkbon.project_number }), _jsx(Veld, { label: "Klant", value: werkbon.customer_name }), _jsx(Veld, { label: "Externe referentie", value: werkbon.externe_referentie }), _jsx(Veld, { label: "First time fix", value: werkbon.first_time_fix == null
|
|
90
|
+
? null
|
|
91
|
+
: werkbon.first_time_fix
|
|
92
|
+
? "Ja"
|
|
93
|
+
: "Nee" }), _jsx(Veld, { label: "Aangemaakt op", value: formatDate(werkbon.created_at) }), _jsx(Veld, { label: "Laatst bijgewerkt", value: formatDate(werkbon.updated_at) })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(MapPin, { className: "w-4 h-4" }), "Werklocatie"] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsx(Veld, { label: "Adres", value: werkbon.work_address }), _jsx(Veld, { label: "Ruimte", value: werkbon.room_number ? `Ruimte ${werkbon.room_number}` : null })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(CalendarClock, { className: "w-4 h-4" }), "Planning"] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [_jsx(Veld, { label: "Geplande start", value: formatDate(werkbon.scheduled_start_time) }), _jsx(Veld, { label: "Gepland einde", value: formatDate(werkbon.scheduled_end_time) }), _jsx(Veld, { label: "Deadline", value: werkbon.deadline_date }), _jsx(Veld, { label: "Geschatte duur", value: werkbon.estimated_duration_minutes != null
|
|
94
|
+
? `${werkbon.estimated_duration_minutes} minuten`
|
|
95
|
+
: null }), _jsx(Veld, { label: "Geschatte dagen", value: werkbon.estimated_days != null ? String(werkbon.estimated_days) : null }), _jsx(Veld, { label: "Afgerond op", value: formatDate(werkbon.afgerond_op) })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(FileText, { className: "w-4 h-4" }), "Omschrijving & notities"] }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx(Veld, { label: "Klachtomschrijving", value: werkbon.complaint_description }), _jsx(Veld, { label: "Taakomschrijving monteur", value: werkbon.technician_task_description }), _jsx(Veld, { label: "Notities monteur", value: werkbon.technician_notes }), _jsx(Veld, { label: "Interne notities", value: werkbon.internal_notes })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(Euro, { className: "w-4 h-4" }), "Financieel"] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsx(Veld, { label: "Budgetmandaat", value: formatEuro(werkbon.budget_mandate_cents) }), _jsx(Veld, { label: "Extra kosten", value: formatEuro(werkbon.extra_costs_cents) })] }) })] })] }));
|
|
96
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
/** `null` => nieuwe klant */
|
|
5
|
+
customerId: string | null;
|
|
6
|
+
onChanged?: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function KlantDialog({ open, onOpenChange, customerId, onChanged }: Props): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=KlantDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDialog.tsx"],"names":[],"mappings":"AAqCA,UAAU,KAAK;IACb,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,6BAA6B;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,KAAK,+BA4L/E"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Link } from "@tanstack/react-router";
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Tabs, TabsContent, TabsList, TabsTrigger, } from "@flowselections/core";
|
|
5
|
+
import { User, Users, MapPin, Wrench, StickyNote, Trash2, Save, ExternalLink, } from "lucide-react";
|
|
6
|
+
import { useKlantForm } from "./useKlantForm";
|
|
7
|
+
import { SegmentToggle, AlgemeneGegevensFields, ContactenFields, LocatiesFields, DynamicFieldsGrid, } from "./KlantFormSections";
|
|
8
|
+
import { KlantGeschiedenisCard } from "./KlantGeschiedenisCard";
|
|
9
|
+
import { KlantNotitiesCard } from "./KlantNotitiesCard";
|
|
10
|
+
import { KlantStatsCard } from "./KlantStatsCard";
|
|
11
|
+
import { SegmentBadge } from "../KlantenListPage";
|
|
12
|
+
export function KlantDialog({ open, onOpenChange, customerId, onChanged }) {
|
|
13
|
+
const [tab, setTab] = useState("gegevens");
|
|
14
|
+
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, } = useKlantForm({
|
|
15
|
+
customerId,
|
|
16
|
+
enabled: open,
|
|
17
|
+
onSaved: () => {
|
|
18
|
+
onChanged?.();
|
|
19
|
+
onOpenChange(false);
|
|
20
|
+
},
|
|
21
|
+
onDeleted: () => {
|
|
22
|
+
onChanged?.();
|
|
23
|
+
onOpenChange(false);
|
|
24
|
+
},
|
|
25
|
+
onNotFound: () => onOpenChange(false),
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (open)
|
|
29
|
+
setTab("gegevens");
|
|
30
|
+
}, [open, customerId]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (form.segment !== "Business" && tab === "contacten")
|
|
33
|
+
setTab("gegevens");
|
|
34
|
+
}, [form.segment, tab]);
|
|
35
|
+
const isBusiness = form.segment === "Business";
|
|
36
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-[720px] max-h-[85vh] overflow-hidden flex flex-col", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2 pr-8", children: [_jsx(User, { className: "w-5 h-5" }), isNew ? "Nieuwe klant" : form.name || "Klant", customerNumber && (_jsx("span", { className: "font-mono text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded", children: customerNumber })), !isNew && _jsx(SegmentBadge, { segment: form.segment })] }), _jsx(DialogDescription, { className: "sr-only", children: "Klantgegevens bewerken" })] }), loading ? (_jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: "Laden\u2026" })) : (_jsxs(Tabs, { value: tab, onValueChange: setTab, className: "flex-1 overflow-hidden flex flex-col", children: [_jsxs(TabsList, { className: `grid w-full ${isBusiness ? "grid-cols-5" : "grid-cols-4"}`, children: [_jsxs(TabsTrigger, { value: "gegevens", title: "Gegevens", className: "gap-1.5 px-1.5 text-xs", children: [_jsx(User, { className: "w-3.5 h-3.5 shrink-0" }), _jsx("span", { className: "hidden lg:inline", children: "Gegevens" })] }), isBusiness && (_jsxs(TabsTrigger, { value: "contacten", title: "Contacten", className: "gap-1.5 px-1.5 text-xs", children: [_jsx(Users, { className: "w-3.5 h-3.5 shrink-0" }), _jsx("span", { className: "hidden lg:inline", children: "Contacten" })] })), _jsxs(TabsTrigger, { value: "locaties", title: "Locaties", className: "gap-1.5 px-1.5 text-xs", children: [_jsx(MapPin, { className: "w-3.5 h-3.5 shrink-0" }), _jsx("span", { className: "hidden lg:inline", children: "Locaties" })] }), _jsxs(TabsTrigger, { value: "werkbonnen", title: "Werkbonnen", disabled: isNew, className: "gap-1.5 px-1.5 text-xs", children: [_jsx(Wrench, { className: "w-3.5 h-3.5 shrink-0" }), _jsx("span", { className: "hidden lg:inline", children: "Werkbonnen" })] }), _jsxs(TabsTrigger, { value: "notities", title: "Notities", disabled: isNew, className: "gap-1.5 px-1.5 text-xs", children: [_jsx(StickyNote, { className: "w-3.5 h-3.5 shrink-0" }), _jsx("span", { className: "hidden lg:inline", children: "Notities" })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto mt-4 pr-1", children: [_jsxs(TabsContent, { value: "gegevens", className: "m-0 space-y-5", children: [!isNew && customerId && _jsx(KlantStatsCard, { customerId: customerId }), _jsx(SegmentToggle, { segment: form.segment, onChange: (s) => setForm((f) => ({ ...f, segment: s })) }), _jsx(AlgemeneGegevensFields, { form: form, setForm: setForm, isNew: isNew }), fields.length > 0 && (_jsxs("div", { className: "space-y-3 pt-2 border-t", children: [_jsx("p", { className: "text-sm font-medium text-foreground pt-3", children: "Aanvullende velden" }), _jsx(DynamicFieldsGrid, { fields: fields, values: values, setValues: setValues })] }))] }), isBusiness && (_jsx(TabsContent, { value: "contacten", className: "m-0", children: _jsx(ContactenFields, { contacts: contacts, addContact: addContact, updateContact: updateContact, removeContact: removeContact }) })), _jsx(TabsContent, { value: "locaties", className: "m-0", children: _jsx(LocatiesFields, { locations: locations, addLocation: addLocation, updateLocation: updateLocation, removeLocation: removeLocation, hideGpsFields: isNew }) }), !isNew && customerId && (_jsxs(_Fragment, { children: [_jsx(TabsContent, { value: "werkbonnen", className: "m-0", children: _jsx(KlantGeschiedenisCard, { customerId: customerId }) }), _jsx(TabsContent, { value: "notities", className: "m-0", children: _jsx(KlantNotitiesCard, { customerId: customerId }) })] }))] }), _jsxs("div", { className: "flex items-center justify-between gap-2 pt-4 border-t mt-4", children: [_jsxs("div", { className: "flex gap-2", children: [!isNew && (_jsxs(Button, { variant: "outline", onClick: remove, className: "gap-2 text-destructive", children: [_jsx(Trash2, { className: "w-4 h-4" }), "Verwijderen"] })), !isNew && customerId && (_jsx(Link, { to: "/installatietechniek-klanten-planner/$id", params: { id: customerId }, children: _jsxs(Button, { variant: "ghost", className: "gap-2", children: [_jsx(ExternalLink, { className: "w-4 h-4" }), _jsx("span", { className: "hidden sm:inline", children: "Volledige pagina" })] }) }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsxs(Button, { onClick: save, disabled: saving, className: "gap-2", children: [_jsx(Save, { className: "w-4 h-4" }), saving ? "Opslaan…" : "Opslaan"] })] })] })] }))] }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DynamicField } from "../../lib/itkClient";
|
|
2
|
+
import type { KlantFormState, NewContact, NewLocation } from "./useKlantForm";
|
|
3
|
+
export declare function Field({ label, children }: {
|
|
4
|
+
label: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function SegmentButton({ active, onClick, icon, label, }: {
|
|
8
|
+
active: boolean;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
icon: React.ReactNode;
|
|
11
|
+
label: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
export declare function SegmentToggle({ segment, onChange, }: {
|
|
14
|
+
segment: KlantFormState["segment"];
|
|
15
|
+
onChange: (s: KlantFormState["segment"]) => void;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export declare function AlgemeneGegevensFields({ form, setForm, isNew, }: {
|
|
18
|
+
form: KlantFormState;
|
|
19
|
+
setForm: React.Dispatch<React.SetStateAction<KlantFormState>>;
|
|
20
|
+
isNew?: boolean;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
export declare function ContactenFields({ contacts, addContact, updateContact, removeContact, }: {
|
|
23
|
+
contacts: NewContact[];
|
|
24
|
+
addContact: () => void;
|
|
25
|
+
updateContact: (idx: number, patch: Partial<NewContact>) => void;
|
|
26
|
+
removeContact: (idx: number) => void;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
export declare function LocatiesFields({ locations, addLocation, updateLocation, removeLocation, hideGpsFields, }: {
|
|
29
|
+
locations: NewLocation[];
|
|
30
|
+
addLocation: () => void;
|
|
31
|
+
updateLocation: (idx: number, patch: Partial<NewLocation>) => void;
|
|
32
|
+
removeLocation: (idx: number) => void;
|
|
33
|
+
hideGpsFields?: boolean;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
35
|
+
export declare function DynamicFieldInput({ field, value, onChange, }: {
|
|
36
|
+
field: DynamicField;
|
|
37
|
+
value: string;
|
|
38
|
+
onChange: (v: string) => void;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
export declare function DynamicFieldsGrid({ fields, values, setValues, }: {
|
|
41
|
+
fields: DynamicField[];
|
|
42
|
+
values: Record<string, string>;
|
|
43
|
+
setValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
|
44
|
+
}): import("react").JSX.Element;
|
|
45
|
+
//# sourceMappingURL=KlantFormSections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantFormSections.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantFormSections.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE9E,wBAAgB,KAAK,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,+BAOtF;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,KAAK,GACN,EAAE;IACD,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,+BAgBA;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACnC,QAAQ,EAAE,CAAC,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC;CAClD,+BAiBA;AAED,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,OAAO,EACP,KAAa,GACd,EAAE;IACD,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC,CAAC;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,+BAqDA;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,UAAU,EACV,aAAa,EACb,aAAa,GACd,EAAE;IACD,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;IACjE,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,+BAqDA;AAED,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAqB,GACtB,EAAE;IACD,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IACnE,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,+BA8FA;AAED,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,YAAY,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,+BA4CA;AAED,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,MAAM,EACN,SAAS,GACV,EAAE;IACD,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CACzE,+BAaA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Input, Label, Textarea, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
|
|
3
|
+
import { Building2, User, Plus, Trash2 } from "lucide-react";
|
|
4
|
+
export function Field({ label, children }) {
|
|
5
|
+
return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs font-medium text-muted-foreground", children: label }), children] }));
|
|
6
|
+
}
|
|
7
|
+
export function SegmentButton({ active, onClick, icon, label, }) {
|
|
8
|
+
return (_jsxs("button", { type: "button", onClick: onClick, className: "flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors " +
|
|
9
|
+
(active
|
|
10
|
+
? "bg-primary text-primary-foreground shadow-sm"
|
|
11
|
+
: "text-muted-foreground hover:text-foreground"), children: [icon, label] }));
|
|
12
|
+
}
|
|
13
|
+
export function SegmentToggle({ segment, onChange, }) {
|
|
14
|
+
return (_jsxs("div", { className: "inline-flex rounded-lg border p-1 bg-muted/40", children: [_jsx(SegmentButton, { active: segment === "Business", onClick: () => onChange("Business"), icon: _jsx(Building2, { className: "w-4 h-4" }), label: "Zakelijk" }), _jsx(SegmentButton, { active: segment === "Private", onClick: () => onChange("Private"), icon: _jsx(User, { className: "w-4 h-4" }), label: "Particulier" })] }));
|
|
15
|
+
}
|
|
16
|
+
export function AlgemeneGegevensFields({ form, setForm, isNew = false, }) {
|
|
17
|
+
return (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [isNew && (_jsx("p", { className: "text-xs text-muted-foreground md:col-span-2", children: "Het klantnummer wordt automatisch toegekend na het opslaan." })), _jsx(Field, { label: "Naam *", children: _jsx(Input, { value: form.name, onChange: (e) => setForm((f) => ({ ...f, name: e.target.value })), placeholder: "Bedrijf of persoonsnaam" }) }), _jsx(Field, { label: "Algemeen e-mailadres *", children: _jsx(Input, { type: "email", value: form.general_email ?? "", onChange: (e) => setForm((f) => ({ ...f, general_email: e.target.value })), placeholder: "info@voorbeeld.nl" }) }), _jsx(Field, { label: "Algemeen telefoonnummer *", children: _jsx(Input, { value: form.general_phone ?? "", onChange: (e) => setForm((f) => ({ ...f, general_phone: e.target.value })), placeholder: "+31 \u2026" }) }), _jsx("div", {}), _jsx(Field, { label: "Factuuradres \u2014 straat *", children: _jsx(Input, { value: form.billing_street ?? "", onChange: (e) => setForm((f) => ({ ...f, billing_street: e.target.value })) }) }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Field, { label: "Postcode *", children: _jsx(Input, { value: form.billing_postcode ?? "", onChange: (e) => setForm((f) => ({ ...f, billing_postcode: e.target.value })) }) }), _jsx(Field, { label: "Plaats *", children: _jsx(Input, { value: form.billing_city ?? "", onChange: (e) => setForm((f) => ({ ...f, billing_city: e.target.value })) }) })] })] }));
|
|
18
|
+
}
|
|
19
|
+
export function ContactenFields({ contacts, addContact, updateContact, removeContact, }) {
|
|
20
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { size: "sm", variant: "outline", onClick: addContact, className: "gap-2", children: [_jsx(Plus, { className: "w-4 h-4" }), "Toevoegen"] }) }), contacts.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen contactpersonen." })), contacts.map((c, idx) => (_jsxs("div", { className: "border rounded-lg p-4 space-y-3 bg-muted/20", children: [_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Field, { label: "Naam", children: _jsx(Input, { value: c.name, onChange: (e) => updateContact(idx, { name: e.target.value }) }) }), _jsx(Field, { label: "Functie", children: _jsx(Input, { value: c.role ?? "", onChange: (e) => updateContact(idx, { role: e.target.value }) }) }), _jsx(Field, { label: "Telefoon", children: _jsx(Input, { value: c.phone ?? "", onChange: (e) => updateContact(idx, { phone: e.target.value }) }) }), _jsx(Field, { label: "E-mail", children: _jsx(Input, { type: "email", value: c.email ?? "", onChange: (e) => updateContact(idx, { email: e.target.value }) }) })] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { size: "sm", variant: "ghost", onClick: () => removeContact(idx), className: "text-destructive gap-2", children: [_jsx(Trash2, { className: "w-4 h-4" }), "Verwijderen"] }) })] }, idx)))] }));
|
|
21
|
+
}
|
|
22
|
+
export function LocatiesFields({ locations, addLocation, updateLocation, removeLocation, hideGpsFields = false, }) {
|
|
23
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { size: "sm", variant: "outline", onClick: addLocation, className: "gap-2", children: [_jsx(Plus, { className: "w-4 h-4" }), "Toevoegen"] }) }), locations.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen werklocaties." })), locations.map((l, idx) => (_jsxs("div", { className: "border rounded-lg p-4 space-y-3 bg-muted/20", children: [_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Field, { label: "Label *", children: _jsx(Input, { value: l.label, onChange: (e) => updateLocation(idx, { label: e.target.value }), placeholder: "Bijv. Hoofdvestiging" }) }), _jsx(Field, { label: "Straat + huisnummer", children: _jsx(Input, { value: l.street ?? "", onChange: (e) => updateLocation(idx, { street: e.target.value }) }) }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(Field, { label: "Postcode", children: _jsx(Input, { value: l.postcode ?? "", onChange: (e) => updateLocation(idx, { postcode: e.target.value }) }) }), _jsx(Field, { label: "Plaats", children: _jsx(Input, { value: l.city ?? "", onChange: (e) => updateLocation(idx, { city: e.target.value }) }) })] }), !hideGpsFields && (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(Field, { label: "Latitude", children: _jsx(Input, { type: "number", step: "any", value: l.latitude ?? "", onChange: (e) => updateLocation(idx, {
|
|
24
|
+
latitude: e.target.value === "" ? null : Number(e.target.value),
|
|
25
|
+
}) }) }), _jsx(Field, { label: "Longitude", children: _jsx(Input, { type: "number", step: "any", value: l.longitude ?? "", onChange: (e) => updateLocation(idx, {
|
|
26
|
+
longitude: e.target.value === "" ? null : Number(e.target.value),
|
|
27
|
+
}) }) })] }))] }), _jsx(Field, { label: "Specifieke instructies", children: _jsx(Textarea, { value: l.instructions ?? "", onChange: (e) => updateLocation(idx, { instructions: e.target.value }), placeholder: "Bijv. Sleutel onder de mat", rows: 2 }) }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { size: "sm", variant: "ghost", onClick: () => removeLocation(idx), className: "text-destructive gap-2", children: [_jsx(Trash2, { className: "w-4 h-4" }), "Verwijderen"] }) })] }, idx)))] }));
|
|
28
|
+
}
|
|
29
|
+
export function DynamicFieldInput({ field, value, onChange, }) {
|
|
30
|
+
const label = field.field_name;
|
|
31
|
+
if (field.field_type === "text") {
|
|
32
|
+
return (_jsx(Field, { label: label, children: _jsx(Input, { value: value, onChange: (e) => onChange(e.target.value) }) }));
|
|
33
|
+
}
|
|
34
|
+
if (field.field_type === "date") {
|
|
35
|
+
return (_jsx(Field, { label: label, children: _jsx(Input, { type: "date", value: value, onChange: (e) => onChange(e.target.value) }) }));
|
|
36
|
+
}
|
|
37
|
+
if (field.field_type === "checkbox") {
|
|
38
|
+
return (_jsxs("div", { className: "flex items-center gap-2 pt-6", children: [_jsx(Checkbox, { checked: value === "true", onCheckedChange: (v) => onChange(v ? "true" : "false") }), _jsx(Label, { className: "text-sm", children: label })] }));
|
|
39
|
+
}
|
|
40
|
+
const options = Array.isArray(field.options) ? field.options : [];
|
|
41
|
+
return (_jsx(Field, { label: label, children: _jsxs(Select, { value: value, onValueChange: onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies\u2026" }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: opt, children: opt }, opt))) })] }) }));
|
|
42
|
+
}
|
|
43
|
+
export function DynamicFieldsGrid({ fields, values, setValues, }) {
|
|
44
|
+
return (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: fields.map((field) => (_jsx(DynamicFieldInput, { field: field, value: values[field.id] ?? "", onChange: (v) => setValues((prev) => ({ ...prev, [field.id]: v })) }, field.id))) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantGeschiedenisCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantGeschiedenisCard.tsx"],"names":[],"mappings":"AASA,UAAU,KAAK;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAiDD,wBAAgB,qBAAqB,CAAC,EAAE,UAAU,EAAE,EAAE,KAAK,+BA0I1D"}
|