@flowselections/klanten-installatie-techniek-planner 1.0.29 → 1.1.2
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/CHANGELOG.md +12 -0
- package/README.md +26 -0
- package/dist-lib/components/KlantenDetailPage.d.ts.map +1 -1
- package/dist-lib/components/KlantenDetailPage.js +4 -3
- package/dist-lib/components/KlantenListPage.d.ts.map +1 -1
- package/dist-lib/components/KlantenListPage.js +20 -11
- package/dist-lib/components/klanten/KlantBeheerActies.d.ts +21 -0
- package/dist-lib/components/klanten/KlantBeheerActies.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantBeheerActies.js +16 -0
- package/dist-lib/components/klanten/KlantDialog.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDialog.js +7 -4
- package/dist-lib/components/klanten/KlantFormSections.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantFormSections.js +2 -2
- package/dist-lib/components/klanten/KlantStatsCard.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantStatsCard.js +4 -1
- package/dist-lib/components/klanten/KlantenSettingsCard.d.ts +2 -0
- package/dist-lib/components/klanten/KlantenSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantenSettingsCard.js +37 -0
- package/dist-lib/components/klanten/KlantenTrendCard.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenTrendCard.js +9 -5
- package/dist-lib/components/klanten/useKlantForm.d.ts +20 -2
- package/dist-lib/components/klanten/useKlantForm.d.ts.map +1 -1
- package/dist-lib/components/klanten/useKlantForm.js +155 -78
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +4 -2
- package/dist-lib/integrations/supabase/types.d.ts +4173 -47
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/itkClient.d.ts +5 -0
- package/dist-lib/lib/itkClient.d.ts.map +1 -1
- package/dist-lib/lib/klantNieuw.test.d.ts +2 -0
- package/dist-lib/lib/klantNieuw.test.d.ts.map +1 -0
- package/dist-lib/lib/klantNieuw.test.js +51 -0
- package/dist-lib/lib/klantRules.d.ts +22 -0
- package/dist-lib/lib/klantRules.d.ts.map +1 -1
- package/dist-lib/lib/klantRules.js +49 -0
- package/dist-lib/lib/klantRules.test.js +26 -0
- package/dist-lib/lib/klantSettings.d.ts +16 -0
- package/dist-lib/lib/klantSettings.d.ts.map +1 -0
- package/dist-lib/lib/klantSettings.js +38 -0
- package/dist-lib/lib/klantStatsClient.d.ts +3 -0
- package/dist-lib/lib/klantStatsClient.d.ts.map +1 -1
- package/dist-lib/lib/klantStatsClient.js +19 -2
- package/dist-lib/lib/statusConfig.d.ts +6 -0
- package/dist-lib/lib/statusConfig.d.ts.map +1 -1
- package/dist-lib/lib/statusConfig.js +3 -0
- package/package.json +27 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
**Migratie: ja. Nieuwe instellingen: ja** (tabel `itk_klanten_settings`, kaart "Klanten").
|
|
5
|
+
|
|
6
|
+
- Nieuwe kolommen `itk_customers.archived_at` en `itk_customers.country` (geen backfill).
|
|
7
|
+
- Opslaan van klant, contactpersonen, werklocaties en dynamische velden in één transactie via `itk_customer_save`. Contactpersonen en locaties worden op id bijgewerkt in plaats van gewist en opnieuw ingevoegd.
|
|
8
|
+
- `itk_customer_delete_hard`: definitief verwijderen wordt geweigerd als er werkbonnen, offertes, facturen of planning aan de klant hangen.
|
|
9
|
+
- `itk_customer_find_duplicates`, `itk_customer_validate`: controle op dubbele klanten en op formaten.
|
|
10
|
+
- Trigram-indexen voor zoeken, en indexen op genormaliseerde KvK, btw, e-mail en postcode (niet uniek).
|
|
11
|
+
- Nieuwe velden KvK-nummer en Land in het formulier.
|
|
12
|
+
- Schakelaars, allemaal standaard uit (= huidig gedrag): `archiveren_aan`, `optimistische_lock_aan`, `formaatcontrole_aan`, `normaliseren_aan`, `dubbelcheck_aan`. Overige instellingen: `standaard_land` (NL), `tijdzone` (Europe/Amsterdam), `zoek_paginagrootte` (50).
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @flowselections/klanten-installatie-techniek-planner
|
|
2
|
+
|
|
3
|
+
Klantenregistratie voor installatietechniek: klanten, contactpersonen, werklocaties, dynamische velden, notities, blacklist, omzet- en winstcijfers.
|
|
4
|
+
|
|
5
|
+
## Installatie
|
|
6
|
+
Voeg de module toe aan een shell met `@flowselections/core`, en voer de migraties uit in `supabase/migrations` (allemaal idempotent). De module gebruikt de Supabase-verbinding van de shell en heeft geen eigen secrets of URL's.
|
|
7
|
+
|
|
8
|
+
## Instellingen (kaart "Klanten", tabel `itk_klanten_settings`)
|
|
9
|
+
| Instelling | Standaard | Effect |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| archiveren_aan | uit | Archiveren in plaats van verwijderen, filter "Toon gearchiveerd", veilig definitief verwijderen |
|
|
12
|
+
| optimistische_lock_aan | uit | Weigert opslaan als een ander de klant intussen wijzigde |
|
|
13
|
+
| formaatcontrole_aan | uit | Controle op e-mail, NL-postcode, KvK (8 cijfers), NL-btw-nummer en telefoon |
|
|
14
|
+
| normaliseren_aan | uit | E-mail in kleine letters, postcode als `1234 AB` |
|
|
15
|
+
| dubbelcheck_aan | uit | Waarschuwing bij dezelfde KvK, hetzelfde btw-nummer, of dezelfde e-mail + postcode |
|
|
16
|
+
| standaard_land | NL | Standaardland voor de formaatcontrole |
|
|
17
|
+
| tijdzone | Europe/Amsterdam | Dag- en maandgrenzen |
|
|
18
|
+
| zoek_paginagrootte | 50 | Klanten per pagina |
|
|
19
|
+
|
|
20
|
+
Ontbreekt de tabel of een waarde, dan geldt het standaardgedrag.
|
|
21
|
+
|
|
22
|
+
## Databasefuncties
|
|
23
|
+
`itk_customer_save`, `itk_customer_delete_hard`, `itk_customer_find_duplicates`, `itk_customer_validate`, `itk_customer_stats`, `itk_top_customers_by_revenue`, `itk_month_trend` en `itk_set_customer_risk`.
|
|
24
|
+
|
|
25
|
+
## Tests
|
|
26
|
+
`bunx vitest run`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantenDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/KlantenDetailPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantenDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/KlantenDetailPage.tsx"],"names":[],"mappings":"AAyBA,wBAAgB,iBAAiB,gCAqMhC"}
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { useNavigate, useParams, Link } from "@tanstack/react-router";
|
|
4
4
|
import { Button, Card, CardContent, CardHeader, CardTitle, Separator, } from "@flowselections/core";
|
|
5
|
-
import { ArrowLeft,
|
|
5
|
+
import { ArrowLeft, Save, MapPin } from "lucide-react";
|
|
6
6
|
import { useKlantForm } from "./klanten/useKlantForm";
|
|
7
|
+
import { KlantBeheerActies, KlantDubbelMelding } from "./klanten/KlantBeheerActies";
|
|
7
8
|
import { SegmentToggle, AlgemeneGegevensFields, ContactenFields, LocatiesFields, DynamicFieldsGrid, } from "./klanten/KlantFormSections";
|
|
8
9
|
import { KlantGeschiedenisCard } from "./klanten/KlantGeschiedenisCard";
|
|
9
10
|
import { KlantNotitiesCard } from "./klanten/KlantNotitiesCard";
|
|
@@ -13,7 +14,7 @@ export function KlantenDetailPage() {
|
|
|
13
14
|
const { id } = useParams({ from: "/_authenticated/installatietechniek-klanten-planner/$id" });
|
|
14
15
|
const navigate = useNavigate();
|
|
15
16
|
const [riskKey, setRiskKey] = useState(0);
|
|
16
|
-
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, reload, } = useKlantForm({
|
|
17
|
+
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, settings, archivedAt, busy, unarchive, hardDelete, duplicates, confirmDuplicate, dismissDuplicates, reload, } = useKlantForm({
|
|
17
18
|
customerId: id,
|
|
18
19
|
onSaved: (newId) => {
|
|
19
20
|
if (newId !== id) {
|
|
@@ -29,5 +30,5 @@ export function KlantenDetailPage() {
|
|
|
29
30
|
if (loading) {
|
|
30
31
|
return (_jsx("main", { className: "p-6", children: _jsx("p", { className: "text-muted-foreground", children: "Laden\u2026" }) }));
|
|
31
32
|
}
|
|
32
|
-
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 }))
|
|
33
|
+
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 })), archivedAt && _jsx("span", { className: "text-xs rounded bg-muted px-2 py-1 text-muted-foreground", children: "Gearchiveerd" }), !isNew && form.vat_reverse_charge && (_jsx("span", { className: "text-xs rounded bg-muted px-2 py-1 text-muted-foreground", children: "Btw verlegd" }))] }), _jsxs("div", { className: "flex gap-2", children: [!isNew && (_jsx(KlantBeheerActies, { archiverenAan: settings.archiveren_aan, archivedAt: archivedAt, busy: busy, remove: remove, unarchive: unarchive, hardDelete: hardDelete })), _jsxs(Button, { onClick: () => save(), disabled: saving || busy, 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(KlantRisicoBannerById, { customerId: id, refreshKey: riskKey }), !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, onStatusChanged: () => setRiskKey((k) => k + 1) })] })), _jsx(KlantDubbelMelding, { duplicates: duplicates, saving: saving, onOpen: (dupId) => void navigate({ to: "/installatietechniek-klanten-planner/$id", params: { id: dupId } }), onConfirm: () => void confirmDuplicate(), onCancel: dismissDuplicates }), _jsx(Separator, {}), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { onClick: () => save(), disabled: saving || busy, className: "gap-2", children: [_jsx(Save, { className: "w-4 h-4" }), saving ? "Opslaan…" : "Opslaan"] }) })] }));
|
|
33
34
|
}
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"KlantenListPage.d.ts","sourceRoot":"","sources":["../../src/components/KlantenListPage.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAa3E,wBAAgB,eAAe,gCA0P9B;AAED,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,+BAkBrE"}
|
|
@@ -4,11 +4,13 @@ import { Button, Card, CardContent, Input, Table, TableBody, TableCell, TableHea
|
|
|
4
4
|
import { Plus, Search, Building2, User } from "lucide-react";
|
|
5
5
|
import { db } from "../lib/itkClient";
|
|
6
6
|
import { fetchKlantStats, fetchTopKlanten, formatEuro } from "../lib/klantStatsClient";
|
|
7
|
+
import { buildKlantSearchFilter } from "../lib/klantRules";
|
|
8
|
+
import { fetchKlantenSettings, DEFAULT_KLANTEN_SETTINGS } from "../lib/klantSettings";
|
|
9
|
+
import { Checkbox, Label } from "@flowselections/core";
|
|
7
10
|
import { KlantDialog } from "./klanten/KlantDialog";
|
|
8
11
|
import { KlantRisicoBadge } from "./klanten/KlantRisicoBanner";
|
|
9
12
|
import { KlantenPodium } from "./klanten/KlantenPodium";
|
|
10
13
|
import { KlantenTrendCard } from "./klanten/KlantenTrendCard";
|
|
11
|
-
const PAGE_SIZE = 50;
|
|
12
14
|
export function KlantenListPage() {
|
|
13
15
|
const [customers, setCustomers] = useState([]);
|
|
14
16
|
const [total, setTotal] = useState(0);
|
|
@@ -24,15 +26,23 @@ export function KlantenListPage() {
|
|
|
24
26
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
25
27
|
const [selectedId, setSelectedId] = useState(null);
|
|
26
28
|
const reqId = useRef(0);
|
|
29
|
+
const [settings, setSettings] = useState(null);
|
|
30
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
31
|
+
const PAGE_SIZE = (settings ?? DEFAULT_KLANTEN_SETTINGS).zoek_paginagrootte;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
void fetchKlantenSettings().then(setSettings);
|
|
34
|
+
}, []);
|
|
27
35
|
useEffect(() => {
|
|
28
36
|
const t = setTimeout(() => setDebounced(search.trim()), 300);
|
|
29
37
|
return () => clearTimeout(t);
|
|
30
38
|
}, [search]);
|
|
31
|
-
useEffect(() => setPage(0), [debounced, segmentFilter]);
|
|
39
|
+
useEffect(() => setPage(0), [debounced, segmentFilter, showArchived]);
|
|
32
40
|
useEffect(() => {
|
|
41
|
+
if (!settings)
|
|
42
|
+
return;
|
|
33
43
|
void load();
|
|
34
44
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
-
}, [page, debounced, segmentFilter]);
|
|
45
|
+
}, [page, debounced, segmentFilter, settings, showArchived]);
|
|
36
46
|
useEffect(() => {
|
|
37
47
|
void loadTop();
|
|
38
48
|
}, []);
|
|
@@ -59,17 +69,16 @@ export function KlantenListPage() {
|
|
|
59
69
|
.range(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE - 1);
|
|
60
70
|
if (segmentFilter !== "all")
|
|
61
71
|
query = query.eq("segment", segmentFilter);
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
}
|
|
72
|
+
if (settings?.archiveren_aan && !showArchived)
|
|
73
|
+
query = query.is("archived_at", null);
|
|
74
|
+
const filter = buildKlantSearchFilter(debounced);
|
|
75
|
+
if (filter)
|
|
76
|
+
query = query.or(filter);
|
|
69
77
|
const { data, error, count } = await query;
|
|
70
78
|
if (my !== reqId.current)
|
|
71
79
|
return;
|
|
72
80
|
if (error) {
|
|
81
|
+
console.error("[klanten] lijst laden mislukt", error);
|
|
73
82
|
setLoadError(true);
|
|
74
83
|
setCustomers([]);
|
|
75
84
|
toast.error("Klanten konden niet geladen worden");
|
|
@@ -97,7 +106,7 @@ export function KlantenListPage() {
|
|
|
97
106
|
const filtered = customers;
|
|
98
107
|
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
99
108
|
const statCell = (id, pick) => stats[id] ? pick(stats[id]) : statsFailed ? "n.b." : "—";
|
|
100
|
-
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 ?? "—" }), _jsxs(TableCell, { className: "font-medium text-foreground", children: [c.name, _jsx(KlantRisicoBadge, { status: c.risk_status })] }), _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(KlantenTrendCard, {}), _jsx(KlantDialog, { open: dialogOpen, onOpenChange: setDialogOpen, customerId: selectedId, onChanged: () => { void load(); void loadTop(); } })] }));
|
|
109
|
+
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" })] })] }), settings?.archiveren_aan && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "toon-gearchiveerd", checked: showArchived, onCheckedChange: (v) => setShowArchived(!!v) }), _jsx(Label, { htmlFor: "toon-gearchiveerd", className: "text-sm", children: "Toon gearchiveerd" })] }))] }), _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 ?? "—" }), _jsxs(TableCell, { className: "font-medium text-foreground", children: [c.name, c.archived_at && (_jsx("span", { className: "ml-2 text-[10px] uppercase tracking-wide rounded bg-muted px-1.5 py-0.5 text-muted-foreground", children: "Gearchiveerd" })), _jsx(KlantRisicoBadge, { status: c.risk_status }), c.vat_reverse_charge && (_jsx("span", { className: "ml-2 text-[10px] uppercase tracking-wide rounded bg-muted px-1.5 py-0.5 text-muted-foreground", children: "Btw verlegd" }))] }), _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(KlantenTrendCard, {}), _jsx(KlantDialog, { open: dialogOpen, onOpenChange: setDialogOpen, customerId: selectedId, onChanged: () => { void load(); void loadTop(); } })] }));
|
|
101
110
|
}
|
|
102
111
|
export function SegmentBadge({ segment }) {
|
|
103
112
|
if (segment === "Business") {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DuplicateHit } from "./useKlantForm";
|
|
2
|
+
interface ActiesProps {
|
|
3
|
+
archiverenAan: boolean;
|
|
4
|
+
archivedAt: string | null;
|
|
5
|
+
busy: boolean;
|
|
6
|
+
remove: () => void;
|
|
7
|
+
unarchive: () => void;
|
|
8
|
+
hardDelete: () => void;
|
|
9
|
+
}
|
|
10
|
+
/** Verwijderen (schakelaar uit) of Archiveren/Terugzetten + Definitief verwijderen (schakelaar aan). */
|
|
11
|
+
export declare function KlantBeheerActies({ archiverenAan, archivedAt, busy, remove, unarchive, hardDelete }: ActiesProps): import("react").JSX.Element;
|
|
12
|
+
interface DubbelProps {
|
|
13
|
+
duplicates: DuplicateHit[];
|
|
14
|
+
saving: boolean;
|
|
15
|
+
onOpen: (id: string) => void;
|
|
16
|
+
onConfirm: () => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function KlantDubbelMelding({ duplicates, saving, onOpen, onConfirm, onCancel }: DubbelProps): import("react").JSX.Element | null;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=KlantBeheerActies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantBeheerActies.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantBeheerActies.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,UAAU,WAAW;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wGAAwG;AACxG,wBAAgB,iBAAiB,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,WAAW,+BA4BhH;AAED,UAAU,WAAW;IACnB,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAID,wBAAgB,kBAAkB,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,WAAW,sCAsBlG"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@flowselections/core";
|
|
3
|
+
import { Archive, ArchiveRestore, Trash2 } from "lucide-react";
|
|
4
|
+
/** Verwijderen (schakelaar uit) of Archiveren/Terugzetten + Definitief verwijderen (schakelaar aan). */
|
|
5
|
+
export function KlantBeheerActies({ archiverenAan, archivedAt, busy, remove, unarchive, hardDelete }) {
|
|
6
|
+
if (!archiverenAan) {
|
|
7
|
+
return (_jsxs(Button, { variant: "outline", onClick: remove, disabled: busy, className: "gap-2 text-destructive", children: [_jsx(Trash2, { className: "w-4 h-4" }), "Verwijderen"] }));
|
|
8
|
+
}
|
|
9
|
+
return (_jsxs(_Fragment, { children: [archivedAt ? (_jsxs(Button, { variant: "outline", onClick: unarchive, disabled: busy, className: "gap-2", children: [_jsx(ArchiveRestore, { className: "w-4 h-4" }), "Terugzetten"] })) : (_jsxs(Button, { variant: "outline", onClick: remove, disabled: busy, className: "gap-2", children: [_jsx(Archive, { className: "w-4 h-4" }), "Archiveren"] })), _jsxs(Button, { variant: "ghost", onClick: hardDelete, disabled: busy, className: "gap-2 text-destructive", children: [_jsx(Trash2, { className: "w-4 h-4" }), _jsx("span", { className: "hidden sm:inline", children: "Definitief verwijderen" })] })] }));
|
|
10
|
+
}
|
|
11
|
+
const REDEN = { kvk: "zelfde KvK-nummer", btw: "zelfde btw-nummer", email_postcode: "zelfde e-mail en postcode" };
|
|
12
|
+
export function KlantDubbelMelding({ duplicates, saving, onOpen, onConfirm, onCancel }) {
|
|
13
|
+
if (!Array.isArray(duplicates) || duplicates.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
return (_jsxs("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 space-y-2 text-sm", children: [_jsx("p", { className: "font-medium text-foreground", children: "Mogelijk bestaat deze klant al" }), _jsx("ul", { className: "space-y-1", children: duplicates.map((d) => (_jsxs("li", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("span", { children: [d.customer_number ? _jsx("span", { className: "font-mono text-xs text-muted-foreground mr-2", children: d.customer_number }) : null, d.name, " ", _jsxs("span", { className: "text-muted-foreground", children: ["(", REDEN[d.reden] ?? d.reden, ")"] })] }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => onOpen(d.id), children: "Bestaande openen" })] }, d.id))) }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: onCancel, children: "Annuleren" }), _jsx(Button, { size: "sm", onClick: onConfirm, disabled: saving, children: "Toch aanmaken" })] })] }));
|
|
16
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDialog.tsx"],"names":[],"mappings":"AAwCA,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,+BAsN/E"}
|
|
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Link } from "@tanstack/react-router";
|
|
4
4
|
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Tabs, TabsContent, TabsList, TabsTrigger, } from "@flowselections/core";
|
|
5
|
-
import { User, Users, MapPin, Wrench, StickyNote,
|
|
5
|
+
import { User, Users, MapPin, Wrench, StickyNote, Save, ExternalLink, } from "lucide-react";
|
|
6
6
|
import { useKlantForm } from "./useKlantForm";
|
|
7
|
+
import { KlantBeheerActies, KlantDubbelMelding } from "./KlantBeheerActies";
|
|
8
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
7
9
|
import { SegmentToggle, AlgemeneGegevensFields, ContactenFields, LocatiesFields, DynamicFieldsGrid, } from "./KlantFormSections";
|
|
8
10
|
import { KlantGeschiedenisCard } from "./KlantGeschiedenisCard";
|
|
9
11
|
import { KlantNotitiesCard } from "./KlantNotitiesCard";
|
|
@@ -13,7 +15,8 @@ import { SegmentBadge } from "../KlantenListPage";
|
|
|
13
15
|
export function KlantDialog({ open, onOpenChange, customerId, onChanged }) {
|
|
14
16
|
const [tab, setTab] = useState("gegevens");
|
|
15
17
|
const [riskKey, setRiskKey] = useState(0);
|
|
16
|
-
const
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, settings, archivedAt, busy, unarchive, hardDelete, duplicates, confirmDuplicate, dismissDuplicates, } = useKlantForm({
|
|
17
20
|
customerId,
|
|
18
21
|
enabled: open,
|
|
19
22
|
onSaved: () => {
|
|
@@ -35,8 +38,8 @@ export function KlantDialog({ open, onOpenChange, customerId, onChanged }) {
|
|
|
35
38
|
setTab("gegevens");
|
|
36
39
|
}, [form.segment, tab]);
|
|
37
40
|
const isBusiness = form.segment === "Business";
|
|
38
|
-
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" })] }), !isNew && customerId && !loading && (_jsx(KlantRisicoBannerById, { customerId: customerId, refreshKey: riskKey })), 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, onStatusChanged: () => {
|
|
41
|
+
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 }), archivedAt && _jsx("span", { className: "text-xs rounded bg-muted px-2 py-0.5 text-muted-foreground", children: "Gearchiveerd" })] }), _jsx(DialogDescription, { className: "sr-only", children: "Klantgegevens bewerken" })] }), !isNew && customerId && !loading && (_jsx(KlantRisicoBannerById, { customerId: customerId, refreshKey: riskKey })), 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, onStatusChanged: () => {
|
|
39
42
|
setRiskKey((k) => k + 1);
|
|
40
43
|
onChanged?.();
|
|
41
|
-
} }) })] }))] }), _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 && (
|
|
44
|
+
} }) })] }))] }), _jsx("div", { className: "pt-3", children: _jsx(KlantDubbelMelding, { duplicates: duplicates, saving: saving, onOpen: (dupId) => { onOpenChange(false); void navigate({ to: "/installatietechniek-klanten-planner/$id", params: { id: dupId } }); }, onConfirm: () => void confirmDuplicate(), onCancel: dismissDuplicates }) }), _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 && (_jsx(KlantBeheerActies, { archiverenAan: settings.archiveren_aan, archivedAt: archivedAt, busy: busy, remove: remove, unarchive: unarchive, hardDelete: hardDelete })), !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 || busy, className: "gap-2", children: [_jsx(Save, { className: "w-4 h-4" }), saving ? "Opslaan…" : "Opslaan"] })] })] })] }))] }) }));
|
|
42
45
|
}
|
|
@@ -1 +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,+
|
|
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,+BAiGA;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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Input, Label, Textarea, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
|
|
3
3
|
import { Building2, User, Plus, Trash2 } from "lucide-react";
|
|
4
4
|
export function Field({ label, children }) {
|
|
@@ -14,7 +14,7 @@ export function SegmentToggle({ segment, onChange, }) {
|
|
|
14
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
15
|
}
|
|
16
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 })) }) })] })] }));
|
|
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 })) }) })] }), _jsx(Field, { label: "Land", children: _jsx(Input, { value: form.country ?? "", onChange: (e) => setForm((f) => ({ ...f, country: e.target.value.toUpperCase() || null })), placeholder: "NL", maxLength: 2 }) }), form.segment === "Business" ? (_jsx(Field, { label: "KvK-nummer", children: _jsx(Input, { value: form.kvk_number ?? "", onChange: (e) => setForm((f) => ({ ...f, kvk_number: e.target.value })), placeholder: "12345678", inputMode: "numeric" }) })) : (_jsx("div", { className: "hidden md:block" })), form.segment === "Business" && (_jsxs(_Fragment, { children: [_jsx(Field, { label: form.vat_reverse_charge ? "Btw-identificatienummer *" : "Btw-identificatienummer", children: _jsx(Input, { value: form.vat_number ?? "", onChange: (e) => setForm((f) => ({ ...f, vat_number: e.target.value })), placeholder: "NL123456789B01" }) }), _jsxs("div", { className: "flex items-start gap-2 pt-6", children: [_jsx(Checkbox, { id: "vat-reverse-charge", checked: !!form.vat_reverse_charge, onCheckedChange: (v) => setForm((f) => ({ ...f, vat_reverse_charge: !!v })) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "vat-reverse-charge", className: "text-sm", children: "Btw automatisch verleggen" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Nieuwe facturen krijgen standaard \"btw verlegd\". Per factuur blijft dit aan te passen." })] })] })] }))] }));
|
|
18
18
|
}
|
|
19
19
|
export function ContactenFields({ contacts, addContact, updateContact, removeContact, }) {
|
|
20
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)))] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantStatsCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantStatsCard.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,+
|
|
1
|
+
{"version":3,"file":"KlantStatsCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantStatsCard.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,+BAqGpE"}
|
|
@@ -27,5 +27,8 @@ export function KlantStatsCard({ customerId }) {
|
|
|
27
27
|
? berekenWinst({ omzetCents: stats.omzetCents, materiaalCents: stats.materiaalKostenCents, arbeidCents: stats.arbeidKostenCents })
|
|
28
28
|
: null;
|
|
29
29
|
const zonderLoon = stats?.monteursZonderLoon ?? [];
|
|
30
|
-
return (_jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "grid grid-cols-2 lg:grid-cols-3 gap-3", children: [_jsx(Tile, { icon: _jsx(Euro, { className: "w-3.5 h-3.5" }), label: "Totale omzet", children: _jsx("p", { className: stats ? big : muted, children: stats ? formatEuro(stats.omzetCents) : placeholder }) }), _jsxs(Tile, { icon: _jsx(Wrench, { className: "w-3.5 h-3.5" }), label: "Aantal klussen", children: [_jsx("p", { className: stats ? big : muted, children: stats ? stats.klussen : placeholder }), stats && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [stats.open, " open \u00B7 ", stats.bezig, " bezig \u00B7 ", stats.afgerond, " afgerond"] }))] }), _jsx(Tile, { icon: _jsx(Clock, { className: "w-3.5 h-3.5" }), label: "Te laat betaald", children: stats && stats.teLaat != null ? (_jsxs(_Fragment, { children: [_jsx("p", { className: stats.teLaat > 0 ? `${big} text-destructive` : big, children: stats.teLaat }), (stats.teLaatOpen ?? 0) > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["waarvan ", stats.teLaatOpen, " nog open"] }))] })) : (_jsx("p", { className: muted, children: stats ? "n.b." : placeholder })) }), _jsx(Tile, { icon: _jsx(CalendarClock, { className: "w-3.5 h-3.5" }), label: "Gem. betaaltermijn", children: stats && stats.gemBetaaltermijnDagen != null ? (_jsxs("p", { className: big, children: [Math.round(stats.gemBetaaltermijnDagen), " ", Math.round(stats.gemBetaaltermijnDagen) === 1 ? "dag" : "dagen"] })) : (_jsx("p", { className: muted, children: stats ? "n.b." : placeholder })) }), _jsxs("div", { className: "col-span-2 rounded-md border p-3 min-w-0", children: [_jsx("div", { className: "flex items-center justify-between gap-2", children: _jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [_jsx(TrendingUp, { className: "w-3.5 h-3.5" }), " Winstgevendheid"] }) }), winst ? (_jsxs(_Fragment, { children: [
|
|
30
|
+
return (_jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "grid grid-cols-2 lg:grid-cols-3 gap-3", children: [_jsx(Tile, { icon: _jsx(Euro, { className: "w-3.5 h-3.5" }), label: "Totale omzet", children: _jsx("p", { className: stats ? big : muted, children: stats ? formatEuro(stats.omzetCents) : placeholder }) }), _jsxs(Tile, { icon: _jsx(Wrench, { className: "w-3.5 h-3.5" }), label: "Aantal klussen", children: [_jsx("p", { className: stats ? big : muted, children: stats ? stats.klussen : placeholder }), stats && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [stats.open, " open \u00B7 ", stats.bezig, " bezig \u00B7 ", stats.afgerond, " afgerond"] }))] }), _jsx(Tile, { icon: _jsx(Clock, { className: "w-3.5 h-3.5" }), label: "Te laat betaald", children: stats && stats.teLaat != null ? (_jsxs(_Fragment, { children: [_jsx("p", { className: stats.teLaat > 0 ? `${big} text-destructive` : big, children: stats.teLaat }), (stats.teLaatOpen ?? 0) > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["waarvan ", stats.teLaatOpen, " nog open"] }))] })) : (_jsx("p", { className: muted, children: stats ? "n.b." : placeholder })) }), _jsx(Tile, { icon: _jsx(CalendarClock, { className: "w-3.5 h-3.5" }), label: "Gem. betaaltermijn", children: stats && stats.gemBetaaltermijnDagen != null ? (_jsxs("p", { className: big, children: [Math.round(stats.gemBetaaltermijnDagen), " ", Math.round(stats.gemBetaaltermijnDagen) === 1 ? "dag" : "dagen"] })) : (_jsx("p", { className: muted, children: stats ? "n.b." : placeholder })) }), _jsxs("div", { className: "col-span-2 rounded-md border p-3 min-w-0", children: [_jsx("div", { className: "flex items-center justify-between gap-2", children: _jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [_jsx(TrendingUp, { className: "w-3.5 h-3.5" }), " Winstgevendheid"] }) }), winst ? (_jsxs(_Fragment, { children: [[
|
|
31
|
+
{ label: "Gefactureerd", w: stats?.gefactureerdCents != null ? berekenWinst({ omzetCents: stats.gefactureerdCents, materiaalCents: stats.materiaalKostenCents, arbeidCents: stats.arbeidKostenCents }) : null, uitleg: "alle verstuurde facturen" },
|
|
32
|
+
{ label: "Cashflow", w: winst, uitleg: "alleen betaalde facturen" },
|
|
33
|
+
].map((r) => (_jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 pt-1", children: [_jsx("span", { className: "w-24 text-sm text-muted-foreground", children: r.label }), r.w ? (_jsxs("span", { className: r.w.winstCents < 0 ? `${big} text-destructive` : big, children: [formatEuro(r.w.winstCents), r.w.marge != null && (_jsxs("span", { className: "ml-2 text-sm font-normal text-muted-foreground", children: [Math.round(r.w.marge * 100), "%"] }))] })) : (_jsx("span", { className: muted, children: "n.b." })), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["(", r.uitleg, ")"] })] }, r.label))), _jsxs("p", { className: "text-xs text-muted-foreground pt-1", children: ["Kosten ", formatEuro(winst.kostenCents), ": materialen ", formatEuro(winst.materiaalCents), " \u00B7 uren en reistijd", " ", formatEuro(winst.arbeidCents), " (uurloon per monteur)"] }), zonderLoon.length > 0 && (_jsxs("p", { className: "text-xs text-destructive", children: ["Uurloon ontbreekt voor: ", zonderLoon.join(", ")] }))] })) : (_jsx("p", { className: muted, children: !stats ? placeholder : "Stel uurlonen in bij de urenregistratie" }))] })] }) }));
|
|
31
34
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantenSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenSettingsCard.tsx"],"names":[],"mappings":"AAaA,wBAAgB,mBAAmB,uCAgElC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Checkbox, Input, Label, toast } from "@flowselections/core";
|
|
4
|
+
import { db } from "../../lib/itkClient";
|
|
5
|
+
import { fetchKlantenSettings, mergeKlantenSettings } from "../../lib/klantSettings";
|
|
6
|
+
const SCHAKELAARS = [
|
|
7
|
+
["archiveren_aan", "Archiveren in plaats van verwijderen", "Definitief verwijderen alleen als er geen werkbonnen, offertes, facturen of planning aan de klant hangen."],
|
|
8
|
+
["optimistische_lock_aan", "Waarschuwen bij gelijktijdig bewerken", "Weigert opslaan als iemand anders de klant intussen heeft gewijzigd."],
|
|
9
|
+
["formaatcontrole_aan", "Invoercontrole", "Controleert e-mail, postcode, KvK, btw-nummer en telefoon."],
|
|
10
|
+
["normaliseren_aan", "Gegevens normaliseren", "E-mail in kleine letters, postcode als 1234 AB."],
|
|
11
|
+
["dubbelcheck_aan", "Waarschuwen voor dubbele klanten", "Zelfde KvK, btw-nummer of e-mail + postcode."],
|
|
12
|
+
];
|
|
13
|
+
export function KlantenSettingsCard() {
|
|
14
|
+
const [s, setS] = useState(null);
|
|
15
|
+
const [saving, setSaving] = useState(false);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
void fetchKlantenSettings().then(setS);
|
|
18
|
+
}, []);
|
|
19
|
+
if (!s)
|
|
20
|
+
return null;
|
|
21
|
+
const opslaan = async () => {
|
|
22
|
+
if (saving)
|
|
23
|
+
return;
|
|
24
|
+
setSaving(true);
|
|
25
|
+
const clean = mergeKlantenSettings(s);
|
|
26
|
+
const { error } = await db.from("itk_klanten_settings").upsert({ singleton: true, ...clean }, { onConflict: "singleton" });
|
|
27
|
+
setSaving(false);
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error("[klanten] instellingen opslaan mislukt", error);
|
|
30
|
+
toast.error(`Instellingen opslaan mislukt: ${error.message}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setS(clean);
|
|
34
|
+
toast.success("Instellingen opgeslagen");
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Klanten" }) }), _jsxs(CardContent, { className: "space-y-4", children: [SCHAKELAARS.map(([key, label, uitleg]) => (_jsxs("div", { className: "flex items-start gap-3", children: [_jsx(Checkbox, { id: `ks-${key}`, checked: !!s[key], onCheckedChange: (v) => setS((p) => (p ? { ...p, [key]: !!v } : p)) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `ks-${key}`, className: "text-sm", children: label }), _jsx("p", { className: "text-xs text-muted-foreground", children: uitleg })] })] }, key))), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs", children: "Standaard land" }), _jsx(Input, { value: s.standaard_land, maxLength: 2, onChange: (e) => setS({ ...s, standaard_land: e.target.value.toUpperCase() }) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs", children: "Tijdzone" }), _jsx(Input, { value: s.tijdzone, onChange: (e) => setS({ ...s, tijdzone: e.target.value }) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs", children: "Klanten per pagina" }), _jsx(Input, { type: "number", min: 1, max: 500, value: s.zoek_paginagrootte, onChange: (e) => setS({ ...s, zoek_paginagrootte: Number(e.target.value) }) })] })] }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: opslaan, disabled: saving, children: saving ? "Opslaan…" : "Opslaan" }) })] })] }));
|
|
37
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantenTrendCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenTrendCard.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"KlantenTrendCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenTrendCard.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,gBAAgB,gCAoE/B"}
|
|
@@ -7,15 +7,16 @@ import { berekenWinst, projectMonth, trend } from "../../lib/klantRules";
|
|
|
7
7
|
const pad = (n) => String(n).padStart(2, "0");
|
|
8
8
|
const isoLocal = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
9
9
|
const monthLabel = (iso) => new Intl.DateTimeFormat("nl-NL", { month: "short", year: "numeric" }).format(new Date(`${iso}T12:00:00`));
|
|
10
|
-
function winstVan(p) {
|
|
11
|
-
return berekenWinst({ omzetCents:
|
|
10
|
+
function winstVan(p, opbrengst = p.omzetCents) {
|
|
11
|
+
return berekenWinst({ omzetCents: opbrengst, materiaalCents: p.materiaalKostenCents, arbeidCents: p.arbeidKostenCents })?.winstCents ?? null;
|
|
12
12
|
}
|
|
13
|
-
function TrendTile({ titel, vorig, verwachting, vorigLabel, nuLabel, melding, }) {
|
|
13
|
+
function TrendTile({ titel, vorig, verwachting, vorigLabel, nuLabel, melding, bare, }) {
|
|
14
14
|
const ok = melding == null && vorig != null && verwachting != null;
|
|
15
15
|
const t = ok ? trend(verwachting, vorig) : null;
|
|
16
16
|
const up = (t?.verschilCents ?? 0) >= 0;
|
|
17
17
|
const Icon = up ? TrendingUp : TrendingDown;
|
|
18
|
-
|
|
18
|
+
const inner = (_jsxs("div", { className: "p-4 space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("h3", { className: "text-sm font-medium text-muted-foreground", children: titel }), t && t.procent != null && (_jsxs("span", { className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums ${up ? "bg-[var(--success-soft)] text-[var(--success-soft-foreground)]" : "bg-destructive/10 text-destructive"}`, children: [_jsx(Icon, { className: "w-3.5 h-3.5" }), up ? "+" : "−", Math.abs(Math.round(t.procent * 100)), "%"] })), t && t.procent == null && _jsx("span", { className: "text-xs text-muted-foreground", children: "geen vergelijking" })] }), ok ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-2xl font-semibold text-foreground tabular-nums", children: formatEuro(verwachting) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Verwachting ", nuLabel] }), _jsxs("p", { className: "pt-2 text-sm text-muted-foreground tabular-nums", children: [vorigLabel, ": ", _jsx("span", { className: "text-foreground", children: formatEuro(vorig) })] }), _jsxs("p", { className: `text-sm tabular-nums ${up ? "text-[var(--success-text)]" : "text-destructive"}`, children: [up ? "+" : "−", " ", formatEuro(Math.abs(t.verschilCents)), " t.o.v. vorig jaar"] })] })) : (_jsx("p", { className: "pt-2 text-sm text-muted-foreground", children: melding }))] }));
|
|
19
|
+
return bare ? inner : _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: inner }) });
|
|
19
20
|
}
|
|
20
21
|
export function KlantenTrendCard() {
|
|
21
22
|
const [data, setData] = useState(null);
|
|
@@ -42,11 +43,14 @@ export function KlantenTrendCard() {
|
|
|
42
43
|
const wNuKosten = wNuRaw == null ? null : data.nu.omzetCents - wNuRaw;
|
|
43
44
|
const winstNu = wNuKosten == null ? null : omzetNu - projectMonth(wNuKosten, dag, dagen);
|
|
44
45
|
const winstVorig = winstVan(data.vorigJaar);
|
|
46
|
+
const fNu = data.nu.gefactureerdCents, fVorig = data.vorigJaar.gefactureerdCents;
|
|
47
|
+
const gWinstNu = wNuKosten == null || fNu == null ? null : projectMonth(fNu, dag, dagen) - projectMonth(wNuKosten, dag, dagen);
|
|
48
|
+
const gWinstVorig = fVorig == null ? null : winstVan(data.vorigJaar, fVorig);
|
|
45
49
|
const zonderLoon = Array.from(new Set([...data.nu.monteursZonderLoon, ...data.vorigJaar.monteursZonderLoon]));
|
|
46
50
|
const vorigLabel = monthLabel(data.vorigJaar.van);
|
|
47
51
|
const nuLabel = monthLabel(data.nu.van);
|
|
48
52
|
const winstMelding = winstNu != null && winstVorig != null
|
|
49
53
|
? undefined
|
|
50
54
|
: "Stel uurlonen in bij de urenregistratie";
|
|
51
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsx(TrendTile, { titel: "Omzet", vorig: omzetVorig, verwachting: omzetNu, vorigLabel: vorigLabel, nuLabel: nuLabel }), _jsx(TrendTile, { titel: "Winst", vorig: winstVorig ?? undefined, verwachting: winstNu ?? undefined, vorigLabel: vorigLabel, nuLabel: nuLabel, melding: winstMelding })] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [zonderLoon.length > 0 && _jsxs("span", { className: "text-destructive", children: ["Uurloon ontbreekt voor: ", zonderLoon.join(", "), ". "] }), "Verwachting = stand t/m vandaag (", dag, " van ", dagen, " dagen) doorgetrokken naar de hele maand, vergeleken met dezelfde maand vorig jaar. Alle klanten samen."] })] }));
|
|
55
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsx(TrendTile, { titel: "Omzet", vorig: omzetVorig, verwachting: omzetNu, vorigLabel: vorigLabel, nuLabel: nuLabel }), _jsx(Card, { children: _jsxs(CardContent, { className: "p-0 divide-y divide-border", children: [_jsx(TrendTile, { bare: true, titel: "Winst \u2014 gefactureerd (alle verstuurde facturen)", vorig: gWinstVorig ?? undefined, verwachting: gWinstNu ?? undefined, vorigLabel: vorigLabel, nuLabel: nuLabel, melding: winstMelding ?? (gWinstNu == null || gWinstVorig == null ? "Niet beschikbaar" : undefined) }), _jsx(TrendTile, { bare: true, titel: "Winst \u2014 cashflow (alleen betaalde facturen)", vorig: winstVorig ?? undefined, verwachting: winstNu ?? undefined, vorigLabel: vorigLabel, nuLabel: nuLabel, melding: winstMelding })] }) })] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [zonderLoon.length > 0 && _jsxs("span", { className: "text-destructive", children: ["Uurloon ontbreekt voor: ", zonderLoon.join(", "), ". "] }), "Verwachting = stand t/m vandaag (", dag, " van ", dagen, " dagen) doorgetrokken naar de hele maand, vergeleken met dezelfde maand vorig jaar. Alle klanten samen."] })] }));
|
|
52
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type KlantenSettings } from "../../lib/klantSettings";
|
|
1
2
|
import { type Customer, type ContactPerson, type WorkLocation, type DynamicField } from "../../lib/itkClient";
|
|
2
|
-
export type KlantFormState = Omit<Customer, "id" | "created_at" | "updated_at" | "customer_number">;
|
|
3
|
+
export type KlantFormState = Omit<Customer, "id" | "created_at" | "updated_at" | "customer_number" | "archived_at">;
|
|
3
4
|
export type NewContact = Omit<ContactPerson, "id" | "customer_id"> & {
|
|
4
5
|
id?: string;
|
|
5
6
|
};
|
|
@@ -7,6 +8,12 @@ export type NewLocation = Omit<WorkLocation, "id" | "customer_id"> & {
|
|
|
7
8
|
id?: string;
|
|
8
9
|
};
|
|
9
10
|
export declare const emptyCustomer: KlantFormState;
|
|
11
|
+
export interface DuplicateHit {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
customer_number: string | null;
|
|
15
|
+
reden: string;
|
|
16
|
+
}
|
|
10
17
|
interface Options {
|
|
11
18
|
/** `null` or "nieuw" => create mode */
|
|
12
19
|
customerId: string | null;
|
|
@@ -34,9 +41,20 @@ export declare function useKlantForm({ customerId, enabled, onSaved, onDeleted,
|
|
|
34
41
|
fields: DynamicField[];
|
|
35
42
|
values: Record<string, string>;
|
|
36
43
|
setValues: import("react").Dispatch<import("react").SetStateAction<Record<string, string>>>;
|
|
37
|
-
save: (
|
|
44
|
+
save: (opts?: {
|
|
45
|
+
skipDuplicateCheck?: boolean;
|
|
46
|
+
}) => Promise<void>;
|
|
38
47
|
remove: () => Promise<void>;
|
|
39
48
|
reload: () => Promise<void>;
|
|
49
|
+
settings: KlantenSettings;
|
|
50
|
+
archivedAt: string | null;
|
|
51
|
+
busy: boolean;
|
|
52
|
+
archive: () => Promise<void>;
|
|
53
|
+
unarchive: () => Promise<void>;
|
|
54
|
+
hardDelete: () => Promise<void>;
|
|
55
|
+
duplicates: DuplicateHit[];
|
|
56
|
+
confirmDuplicate: () => Promise<void>;
|
|
57
|
+
dismissDuplicates: () => void;
|
|
40
58
|
};
|
|
41
59
|
export {};
|
|
42
60
|
//# sourceMappingURL=useKlantForm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useKlantForm.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/useKlantForm.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useKlantForm.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/useKlantForm.ts"],"names":[],"mappings":"AACA,OAAO,EAAkD,KAAK,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG/G,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,QAAQ,EACR,IAAI,GAAG,YAAY,GAAG,YAAY,GAAG,iBAAiB,GAAG,aAAa,CACvE,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,aAAa,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACrF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,aAAa,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,eAAO,MAAM,aAAa,EAAE,cAa3B,CAAC;AAEF,MAAM,WAAW,YAAY;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAIzG,UAAU,OAAO;IACf,uCAAuC;IACvC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,OAAc,EACd,OAAO,EACP,SAAS,EACT,UAAU,GACX,EAAE,OAAO;;;;;;;;;yBA2FoB,MAAM,SAAS,OAAO,CAAC,UAAU,CAAC;yBAElC,MAAM;;;0BAgBL,MAAM,SAAS,OAAO,CAAC,WAAW,CAAC;0BAEnC,MAAM;;;;kBAGR;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE;;;;;;;;;;;;EA6L5D"}
|