@flowselections/klanten-installatie-techniek-planner 1.0.21 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/KlantenDetailPage.d.ts.map +1 -1
- package/dist-lib/components/KlantenDetailPage.js +4 -1
- package/dist-lib/components/KlantenListPage.d.ts.map +1 -1
- package/dist-lib/components/KlantenListPage.js +3 -1
- package/dist-lib/components/klanten/KlantDialog.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDialog.js +6 -1
- package/dist-lib/components/klanten/KlantNotitiesCard.d.ts +2 -1
- package/dist-lib/components/klanten/KlantNotitiesCard.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantNotitiesCard.js +12 -3
- package/dist-lib/components/klanten/KlantRisicoBanner.d.ts +18 -0
- package/dist-lib/components/klanten/KlantRisicoBanner.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantRisicoBanner.js +43 -0
- package/dist-lib/components/klanten/KlantRisicoStatusCard.d.ts +7 -0
- package/dist-lib/components/klanten/KlantRisicoStatusCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantRisicoStatusCard.js +65 -0
- package/dist-lib/components/klanten/KlantStatsCard.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantStatsCard.js +14 -4
- package/dist-lib/components/klanten/KlantenTrendCard.d.ts +2 -0
- package/dist-lib/components/klanten/KlantenTrendCard.d.ts.map +1 -0
- package/dist-lib/components/klanten/KlantenTrendCard.js +52 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/integrations/supabase/client.js +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +9209 -261
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/itkClient.d.ts +3 -1
- package/dist-lib/lib/itkClient.d.ts.map +1 -1
- package/dist-lib/lib/klantRules.d.ts +44 -0
- package/dist-lib/lib/klantRules.d.ts.map +1 -1
- package/dist-lib/lib/klantRules.js +46 -1
- package/dist-lib/lib/klantRules.test.js +59 -0
- package/dist-lib/lib/klantStatsClient.d.ts +25 -0
- package/dist-lib/lib/klantStatsClient.d.ts.map +1 -1
- package/dist-lib/lib/klantStatsClient.js +37 -1
- package/dist-lib/lib/statusConfig.d.ts +4 -0
- package/dist-lib/lib/statusConfig.d.ts.map +1 -1
- package/dist-lib/lib/statusConfig.js +7 -0
- package/package.json +39 -10
|
@@ -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":"AAwBA,wBAAgB,iBAAiB,gCAqLhC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
2
3
|
import { useNavigate, useParams, Link } from "@tanstack/react-router";
|
|
3
4
|
import { Button, Card, CardContent, CardHeader, CardTitle, Separator, } from "@flowselections/core";
|
|
4
5
|
import { ArrowLeft, Trash2, Save, MapPin } from "lucide-react";
|
|
@@ -6,10 +7,12 @@ import { useKlantForm } from "./klanten/useKlantForm";
|
|
|
6
7
|
import { SegmentToggle, AlgemeneGegevensFields, ContactenFields, LocatiesFields, DynamicFieldsGrid, } from "./klanten/KlantFormSections";
|
|
7
8
|
import { KlantGeschiedenisCard } from "./klanten/KlantGeschiedenisCard";
|
|
8
9
|
import { KlantNotitiesCard } from "./klanten/KlantNotitiesCard";
|
|
10
|
+
import { KlantRisicoBannerById } from "./klanten/KlantRisicoBanner";
|
|
9
11
|
import { KlantStatsCard } from "./klanten/KlantStatsCard";
|
|
10
12
|
export function KlantenDetailPage() {
|
|
11
13
|
const { id } = useParams({ from: "/_authenticated/installatietechniek-klanten-planner/$id" });
|
|
12
14
|
const navigate = useNavigate();
|
|
15
|
+
const [riskKey, setRiskKey] = useState(0);
|
|
13
16
|
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, reload, } = useKlantForm({
|
|
14
17
|
customerId: id,
|
|
15
18
|
onSaved: (newId) => {
|
|
@@ -26,5 +29,5 @@ export function KlantenDetailPage() {
|
|
|
26
29
|
if (loading) {
|
|
27
30
|
return (_jsx("main", { className: "p-6", children: _jsx("p", { className: "text-muted-foreground", children: "Laden\u2026" }) }));
|
|
28
31
|
}
|
|
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"] }) })] }));
|
|
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 }))] }), _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(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(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
33
|
}
|
|
@@ -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;AAY3E,wBAAgB,eAAe,gCA2O9B;AAED,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,+BAkBrE"}
|
|
@@ -5,7 +5,9 @@ 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
7
|
import { KlantDialog } from "./klanten/KlantDialog";
|
|
8
|
+
import { KlantRisicoBadge } from "./klanten/KlantRisicoBanner";
|
|
8
9
|
import { KlantenPodium } from "./klanten/KlantenPodium";
|
|
10
|
+
import { KlantenTrendCard } from "./klanten/KlantenTrendCard";
|
|
9
11
|
const PAGE_SIZE = 50;
|
|
10
12
|
export function KlantenListPage() {
|
|
11
13
|
const [customers, setCustomers] = useState([]);
|
|
@@ -95,7 +97,7 @@ export function KlantenListPage() {
|
|
|
95
97
|
const filtered = customers;
|
|
96
98
|
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
97
99
|
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 ?? "—" }),
|
|
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(); } })] }));
|
|
99
101
|
}
|
|
100
102
|
export function SegmentBadge({ segment }) {
|
|
101
103
|
if (segment === "Business") {
|
|
@@ -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":"AAsCA,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,+BAsM/E"}
|
|
@@ -7,10 +7,12 @@ import { useKlantForm } from "./useKlantForm";
|
|
|
7
7
|
import { SegmentToggle, AlgemeneGegevensFields, ContactenFields, LocatiesFields, DynamicFieldsGrid, } from "./KlantFormSections";
|
|
8
8
|
import { KlantGeschiedenisCard } from "./KlantGeschiedenisCard";
|
|
9
9
|
import { KlantNotitiesCard } from "./KlantNotitiesCard";
|
|
10
|
+
import { KlantRisicoBannerById } from "./KlantRisicoBanner";
|
|
10
11
|
import { KlantStatsCard } from "./KlantStatsCard";
|
|
11
12
|
import { SegmentBadge } from "../KlantenListPage";
|
|
12
13
|
export function KlantDialog({ open, onOpenChange, customerId, onChanged }) {
|
|
13
14
|
const [tab, setTab] = useState("gegevens");
|
|
15
|
+
const [riskKey, setRiskKey] = useState(0);
|
|
14
16
|
const { isNew, loading, saving, form, setForm, customerNumber, contacts, addContact, updateContact, removeContact, locations, addLocation, updateLocation, removeLocation, fields, values, setValues, save, remove, } = useKlantForm({
|
|
15
17
|
customerId,
|
|
16
18
|
enabled: open,
|
|
@@ -33,5 +35,8 @@ export function KlantDialog({ open, onOpenChange, customerId, onChanged }) {
|
|
|
33
35
|
setTab("gegevens");
|
|
34
36
|
}, [form.segment, tab]);
|
|
35
37
|
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
|
|
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: () => {
|
|
39
|
+
setRiskKey((k) => k + 1);
|
|
40
|
+
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 && (_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
42
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
customerId: string;
|
|
3
|
+
onStatusChanged?: () => void;
|
|
3
4
|
}
|
|
4
|
-
export declare function KlantNotitiesCard({ customerId }: Props): import("react").JSX.Element;
|
|
5
|
+
export declare function KlantNotitiesCard({ customerId, onStatusChanged }: Props): import("react").JSX.Element;
|
|
5
6
|
export {};
|
|
6
7
|
//# sourceMappingURL=KlantNotitiesCard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantNotitiesCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantNotitiesCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantNotitiesCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantNotitiesCard.tsx"],"names":[],"mappings":"AA6DA,UAAU,KAAK;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,EAAE,KAAK,+BAuLvE"}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Button, Card, CardContent, CardHeader, CardTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, supabase, toast, } from "@flowselections/core";
|
|
4
4
|
import { StickyNote, Trash2, Plus } from "lucide-react";
|
|
5
|
+
import { KlantRisicoStatusCard } from "./KlantRisicoStatusCard";
|
|
5
6
|
import { db } from "../../lib/itkClient";
|
|
6
7
|
const TYPE_OPTIONS = [
|
|
7
8
|
{ value: "general", label: "Algemeen" },
|
|
@@ -10,15 +11,20 @@ const TYPE_OPTIONS = [
|
|
|
10
11
|
{ value: "visit", label: "Bezoek" },
|
|
11
12
|
{ value: "other", label: "Overig" },
|
|
12
13
|
];
|
|
14
|
+
const TYPE_LABELS = {
|
|
15
|
+
general: "Algemeen", call: "Telefonisch", complaint: "Klacht", visit: "Bezoek", other: "Overig",
|
|
16
|
+
risk_status: "Klantstatus",
|
|
17
|
+
};
|
|
13
18
|
const TYPE_STYLE = {
|
|
14
19
|
general: "bg-muted text-muted-foreground",
|
|
15
20
|
call: "bg-blue-500/15 text-blue-400",
|
|
16
21
|
complaint: "bg-red-500/15 text-red-400",
|
|
17
22
|
visit: "bg-emerald-500/15 text-emerald-400",
|
|
18
23
|
other: "bg-slate-500/15 text-slate-400",
|
|
24
|
+
risk_status: "bg-orange-500/15 text-orange-500",
|
|
19
25
|
};
|
|
20
26
|
function typeLabel(t) {
|
|
21
|
-
return
|
|
27
|
+
return TYPE_LABELS[t] ?? t;
|
|
22
28
|
}
|
|
23
29
|
function formatDateTime(iso) {
|
|
24
30
|
try {
|
|
@@ -34,7 +40,7 @@ function formatDateTime(iso) {
|
|
|
34
40
|
return iso;
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
|
-
export function KlantNotitiesCard({ customerId }) {
|
|
43
|
+
export function KlantNotitiesCard({ customerId, onStatusChanged }) {
|
|
38
44
|
const [notes, setNotes] = useState([]);
|
|
39
45
|
const [authors, setAuthors] = useState({});
|
|
40
46
|
const [loading, setLoading] = useState(true);
|
|
@@ -114,6 +120,9 @@ export function KlantNotitiesCard({ customerId }) {
|
|
|
114
120
|
toast.success("Notitie verwijderd");
|
|
115
121
|
void load();
|
|
116
122
|
};
|
|
117
|
-
return (_jsxs(Card, { id: "notities", children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(StickyNote, { className: "w-4 h-4" }), "Notities"] }) }), _jsxs(CardContent, { className: "space-y-4", children: [
|
|
123
|
+
return (_jsxs(Card, { id: "notities", children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(StickyNote, { className: "w-4 h-4" }), "Notities"] }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx(KlantRisicoStatusCard, { customerId: customerId, onChanged: () => {
|
|
124
|
+
void load();
|
|
125
|
+
onStatusChanged?.();
|
|
126
|
+
} }), _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-[200px_1fr] gap-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs font-medium text-muted-foreground", children: "Type" }), _jsxs(Select, { value: noteType, onValueChange: (v) => setNoteType(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: TYPE_OPTIONS.map((o) => (_jsx(SelectItem, { value: o.value, children: o.label }, o.value))) })] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs font-medium text-muted-foreground", children: "Notitie" }), _jsx(Textarea, { value: body, onChange: (e) => setBody(e.target.value), placeholder: "Bijv. Klant gebeld over aflever\u00ADafspraak\u2026", rows: 3 })] })] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { size: "sm", onClick: addNote, disabled: saving, className: "gap-2", children: [_jsx(Plus, { className: "w-4 h-4" }), saving ? "Opslaan…" : "Toevoegen"] }) })] }), loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" })) : notes.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen notities." })) : (_jsx("div", { className: "space-y-2", children: notes.map((n) => (_jsxs("div", { className: "border rounded-lg p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "text-xs px-2 py-0.5 rounded-full font-medium " +
|
|
118
127
|
TYPE_STYLE[n.note_type], children: typeLabel(n.note_type) }), _jsx("span", { className: "text-xs text-muted-foreground", children: formatDateTime(n.created_at) }), n.created_by && authors[n.created_by] && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u00B7 ", authors[n.created_by]] }))] }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => removeNote(n.id), className: "text-destructive gap-2 h-7", children: _jsx(Trash2, { className: "w-3.5 h-3.5" }) })] }), _jsx("p", { className: "text-sm text-foreground/90 mt-2 whitespace-pre-wrap", children: n.body })] }, n.id))) }))] })] }));
|
|
119
128
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
status: unknown;
|
|
3
|
+
reason?: string | null;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Waarschuwingsbalk voor Risicoklant / Geblokkeerd. Toont niets bij een normale klant. */
|
|
7
|
+
export declare function KlantRisicoBanner({ status, reason, className }: Props): import("react").JSX.Element | null;
|
|
8
|
+
export declare function KlantRisicoBadge({ status }: {
|
|
9
|
+
status: unknown;
|
|
10
|
+
}): import("react").JSX.Element | null;
|
|
11
|
+
/** Laadt de klantstatus zelf; handig in pop-up, klantpagina en andere modules. */
|
|
12
|
+
export declare function KlantRisicoBannerById({ customerId, refreshKey, className }: {
|
|
13
|
+
customerId: string;
|
|
14
|
+
refreshKey?: number;
|
|
15
|
+
className?: string;
|
|
16
|
+
}): import("react").JSX.Element | null;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=KlantRisicoBanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantRisicoBanner.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantRisicoBanner.tsx"],"names":[],"mappings":"AAKA,UAAU,KAAK;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAc,EAAE,EAAE,KAAK,sCAyB1E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,sCAY/D;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,EAAE,UAAU,EAAE,UAAc,EAAE,SAAS,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,sCAgB/I"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { db } from "../../lib/itkClient";
|
|
4
|
+
import { AlertTriangle, Ban } from "lucide-react";
|
|
5
|
+
import { normalizeRiskStatus } from "../../lib/klantRules";
|
|
6
|
+
/** Waarschuwingsbalk voor Risicoklant / Geblokkeerd. Toont niets bij een normale klant. */
|
|
7
|
+
export function KlantRisicoBanner({ status, reason, className = "" }) {
|
|
8
|
+
const s = normalizeRiskStatus(status);
|
|
9
|
+
if (s === "none")
|
|
10
|
+
return null;
|
|
11
|
+
const blocked = s === "blocked";
|
|
12
|
+
const Icon = blocked ? Ban : AlertTriangle;
|
|
13
|
+
return (_jsxs("div", { role: "alert", className: `flex items-start gap-3 rounded-md border p-3 text-sm ${blocked
|
|
14
|
+
? "border-destructive/40 bg-destructive/10 text-destructive"
|
|
15
|
+
: "border-orange-500/40 bg-orange-500/10 text-orange-700 dark:text-orange-300"} ${className}`, children: [_jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-semibold", children: blocked
|
|
16
|
+
? "Geblokkeerd — geen nieuwe werkbonnen mogelijk"
|
|
17
|
+
: "Risicoklant — let op bij nieuwe aanvragen" }), reason && _jsxs("p", { className: "mt-0.5 whitespace-pre-wrap text-foreground/80", children: ["Reden: ", reason] })] })] }));
|
|
18
|
+
}
|
|
19
|
+
export function KlantRisicoBadge({ status }) {
|
|
20
|
+
const s = normalizeRiskStatus(status);
|
|
21
|
+
if (s === "none")
|
|
22
|
+
return null;
|
|
23
|
+
return s === "blocked" ? (_jsx("span", { className: "ml-2 rounded-full bg-destructive/15 px-2 py-0.5 text-[10px] font-semibold text-destructive", children: "Geblokkeerd" })) : (_jsx("span", { className: "ml-2 rounded-full bg-orange-500/15 px-2 py-0.5 text-[10px] font-semibold text-orange-700 dark:text-orange-300", children: "Risicoklant" }));
|
|
24
|
+
}
|
|
25
|
+
/** Laadt de klantstatus zelf; handig in pop-up, klantpagina en andere modules. */
|
|
26
|
+
export function KlantRisicoBannerById({ customerId, refreshKey = 0, className }) {
|
|
27
|
+
const [row, setRow] = useState(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let alive = true;
|
|
30
|
+
void db
|
|
31
|
+
.from("itk_customers")
|
|
32
|
+
.select("risk_status, risk_reason")
|
|
33
|
+
.eq("id", customerId)
|
|
34
|
+
.maybeSingle()
|
|
35
|
+
.then(({ data }) => alive && setRow(data ?? null));
|
|
36
|
+
return () => {
|
|
37
|
+
alive = false;
|
|
38
|
+
};
|
|
39
|
+
}, [customerId, refreshKey]);
|
|
40
|
+
if (!row)
|
|
41
|
+
return null;
|
|
42
|
+
return _jsx(KlantRisicoBanner, { status: row.risk_status, reason: row.risk_reason, className: className });
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantRisicoStatusCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantRisicoStatusCard.tsx"],"names":[],"mappings":"AAmBA,UAAU,KAAK;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wBAAgB,qBAAqB,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,KAAK,+BA2GrE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Button, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, supabase, toast, } from "@flowselections/core";
|
|
4
|
+
import { ShieldAlert } from "lucide-react";
|
|
5
|
+
import { db } from "../../lib/itkClient";
|
|
6
|
+
import { CUSTOMER_RISK_LABELS } from "../../lib/statusConfig";
|
|
7
|
+
import { normalizeRiskStatus, riskReasonMissing } from "../../lib/klantRules";
|
|
8
|
+
import { KlantRisicoBanner } from "./KlantRisicoBanner";
|
|
9
|
+
export function KlantRisicoStatusCard({ customerId, onChanged }) {
|
|
10
|
+
const [current, setCurrent] = useState({
|
|
11
|
+
status: "none",
|
|
12
|
+
reason: null,
|
|
13
|
+
});
|
|
14
|
+
const [status, setStatus] = useState("none");
|
|
15
|
+
const [reason, setReason] = useState("");
|
|
16
|
+
const [saving, setSaving] = useState(false);
|
|
17
|
+
const [failed, setFailed] = useState(false);
|
|
18
|
+
const load = async () => {
|
|
19
|
+
const { data, error } = await db
|
|
20
|
+
.from("itk_customers")
|
|
21
|
+
.select("risk_status, risk_reason")
|
|
22
|
+
.eq("id", customerId)
|
|
23
|
+
.maybeSingle();
|
|
24
|
+
if (error) {
|
|
25
|
+
setFailed(true);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setFailed(false);
|
|
29
|
+
const s = normalizeRiskStatus(data?.risk_status);
|
|
30
|
+
setCurrent({ status: s, reason: data?.risk_reason ?? null });
|
|
31
|
+
setStatus(s);
|
|
32
|
+
setReason(data?.risk_reason ?? "");
|
|
33
|
+
};
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
void load();
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
}, [customerId]);
|
|
38
|
+
const dirty = status !== current.status || (status !== "none" && reason.trim() !== (current.reason ?? ""));
|
|
39
|
+
const save = async () => {
|
|
40
|
+
if (riskReasonMissing(status, reason)) {
|
|
41
|
+
toast.error("Vul een interne reden in");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setSaving(true);
|
|
45
|
+
try {
|
|
46
|
+
const { error } = await supabase.rpc("itk_set_customer_risk", {
|
|
47
|
+
_customer_id: customerId,
|
|
48
|
+
_status: status,
|
|
49
|
+
_reason: status === "none" ? null : reason.trim(),
|
|
50
|
+
});
|
|
51
|
+
if (error)
|
|
52
|
+
throw error;
|
|
53
|
+
toast.success("Klantstatus opgeslagen");
|
|
54
|
+
await load();
|
|
55
|
+
onChanged?.();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
toast.error(e?.message ?? "Opslaan mislukt");
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
setSaving(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs("div", { className: "space-y-3 rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [_jsx(ShieldAlert, { className: "h-4 w-4" }), "Blacklist"] }), failed ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Blacklist-status kon niet geladen worden." })) : (_jsxs(_Fragment, { children: [_jsx(KlantRisicoBanner, { status: current.status, reason: current.reason }), _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-[200px_1fr]", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs font-medium text-muted-foreground", children: "Status" }), _jsxs(Select, { value: status, onValueChange: (v) => setStatus(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.keys(CUSTOMER_RISK_LABELS).map((k) => (_jsx(SelectItem, { value: k, children: CUSTOMER_RISK_LABELS[k] }, k))) })] })] }), status !== "none" && (_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground", children: ["Interne reden ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: "Bijv. Factuur na 3 herinneringen niet betaald", rows: 2 })] }))] }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { size: "sm", onClick: save, disabled: saving || !dirty, children: saving ? "Opslaan…" : "Status opslaan" }) })] }))] }));
|
|
65
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantStatsCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantStatsCard.tsx"],"names":[],"mappings":"
|
|
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,+BA0FpE"}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { Euro, Wrench } from "lucide-react";
|
|
4
|
-
import { fetchKlantStats, formatEuro,
|
|
3
|
+
import { CalendarClock, Clock, Euro, TrendingUp, Wrench } from "lucide-react";
|
|
4
|
+
import { emptyStats, fetchKlantStats, formatEuro, } from "../../lib/klantStatsClient";
|
|
5
|
+
import { berekenWinst } from "../../lib/klantRules";
|
|
6
|
+
function Tile({ icon, label, children }) {
|
|
7
|
+
return (_jsxs("div", { className: "rounded-md border p-3 min-w-0", children: [_jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [icon, " ", label] }), children] }));
|
|
8
|
+
}
|
|
9
|
+
const big = "text-xl font-semibold text-foreground tabular-nums";
|
|
10
|
+
const muted = "text-sm text-muted-foreground pt-1";
|
|
5
11
|
export function KlantStatsCard({ customerId }) {
|
|
6
12
|
const [stats, setStats] = useState(null);
|
|
7
13
|
const [failed, setFailed] = useState(false);
|
|
@@ -17,5 +23,9 @@ export function KlantStatsCard({ customerId }) {
|
|
|
17
23
|
};
|
|
18
24
|
}, [customerId]);
|
|
19
25
|
const placeholder = failed ? "Niet beschikbaar" : "…";
|
|
20
|
-
|
|
26
|
+
const winst = stats
|
|
27
|
+
? berekenWinst({ omzetCents: stats.omzetCents, materiaalCents: stats.materiaalKostenCents, arbeidCents: stats.arbeidKostenCents })
|
|
28
|
+
: null;
|
|
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: [_jsxs("p", { className: winst.winstCents < 0 ? `${big} text-destructive` : big, children: [formatEuro(winst.winstCents), winst.marge != null && (_jsxs("span", { className: "ml-2 text-sm font-normal text-muted-foreground", children: [Math.round(winst.marge * 100), "%"] }))] }), _jsxs("p", { className: "text-xs text-muted-foreground", 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" }))] })] }) }));
|
|
21
31
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlantenTrendCard.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenTrendCard.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,gBAAgB,gCAqD/B"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Card, CardContent } from "@flowselections/core";
|
|
4
|
+
import { TrendingDown, TrendingUp } from "lucide-react";
|
|
5
|
+
import { fetchMonthTrend, formatEuro } from "../../lib/klantStatsClient";
|
|
6
|
+
import { berekenWinst, projectMonth, trend } from "../../lib/klantRules";
|
|
7
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8
|
+
const isoLocal = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
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: p.omzetCents, materiaalCents: p.materiaalKostenCents, arbeidCents: p.arbeidKostenCents })?.winstCents ?? null;
|
|
12
|
+
}
|
|
13
|
+
function TrendTile({ titel, vorig, verwachting, vorigLabel, nuLabel, melding, }) {
|
|
14
|
+
const ok = melding == null && vorig != null && verwachting != null;
|
|
15
|
+
const t = ok ? trend(verwachting, vorig) : null;
|
|
16
|
+
const up = (t?.verschilCents ?? 0) >= 0;
|
|
17
|
+
const Icon = up ? TrendingUp : TrendingDown;
|
|
18
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { 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
|
+
}
|
|
20
|
+
export function KlantenTrendCard() {
|
|
21
|
+
const [data, setData] = useState(null);
|
|
22
|
+
const [failed, setFailed] = useState(false);
|
|
23
|
+
const today = new Date();
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let active = true;
|
|
26
|
+
fetchMonthTrend(isoLocal(new Date()))
|
|
27
|
+
.then((d) => active && setData(d))
|
|
28
|
+
.catch(() => active && setFailed(true));
|
|
29
|
+
return () => {
|
|
30
|
+
active = false;
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
const dag = today.getDate();
|
|
34
|
+
const dagen = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
|
35
|
+
if (failed)
|
|
36
|
+
return _jsx("p", { className: "text-sm text-muted-foreground", children: "Maandvergelijking niet beschikbaar" });
|
|
37
|
+
if (!data)
|
|
38
|
+
return _jsx("p", { className: "text-sm text-muted-foreground", children: "Maandvergelijking laden\u2026" });
|
|
39
|
+
const omzetNu = projectMonth(data.nu.omzetCents, dag, dagen);
|
|
40
|
+
const omzetVorig = data.vorigJaar.omzetCents;
|
|
41
|
+
const wNuRaw = winstVan(data.nu);
|
|
42
|
+
const wNuKosten = wNuRaw == null ? null : data.nu.omzetCents - wNuRaw;
|
|
43
|
+
const winstNu = wNuKosten == null ? null : omzetNu - projectMonth(wNuKosten, dag, dagen);
|
|
44
|
+
const winstVorig = winstVan(data.vorigJaar);
|
|
45
|
+
const zonderLoon = Array.from(new Set([...data.nu.monteursZonderLoon, ...data.vorigJaar.monteursZonderLoon]));
|
|
46
|
+
const vorigLabel = monthLabel(data.vorigJaar.van);
|
|
47
|
+
const nuLabel = monthLabel(data.nu.van);
|
|
48
|
+
const winstMelding = winstNu != null && winstVorig != null
|
|
49
|
+
? undefined
|
|
50
|
+
: "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."] })] }));
|
|
52
|
+
}
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { FlowModule } from "@flowselections/core";
|
|
|
2
2
|
export * from "./_core-safelist";
|
|
3
3
|
export { KlantenListPage as KlantenListPagePlanner } from "./components/KlantenListPage";
|
|
4
4
|
export { KlantenDetailPage as KlantenDetailPagePlanner } from "./components/KlantenDetailPage";
|
|
5
|
+
export { KlantRisicoBanner, KlantRisicoBannerById, KlantRisicoBadge } from "./components/klanten/KlantRisicoBanner";
|
|
5
6
|
export { WerkbonDetailPage as WerkbonDetailPagePlanner } from "./components/WerkbonDetailPage";
|
|
6
7
|
export declare const klantenInstallatieTechniekPlannerModule: FlowModule;
|
|
7
8
|
export declare const KlantenInstallatieTechniekPlanner: FlowModule;
|
package/dist-lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,iBAAiB,IAAI,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/F,OAAO,EAAE,iBAAiB,IAAI,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE/F,eAAO,MAAM,uCAAuC,EAAE,UAUrD,CAAC;AAGF,eAAO,MAAM,iCAAiC,YAA0C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,iBAAiB,IAAI,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/F,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AACpH,OAAO,EAAE,iBAAiB,IAAI,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE/F,eAAO,MAAM,uCAAuC,EAAE,UAUrD,CAAC;AAGF,eAAO,MAAM,iCAAiC,YAA0C,CAAC"}
|
package/dist-lib/index.js
CHANGED
|
@@ -5,11 +5,12 @@ import { Users } from "lucide-react";
|
|
|
5
5
|
export * from "./_core-safelist";
|
|
6
6
|
export { KlantenListPage as KlantenListPagePlanner } from "./components/KlantenListPage";
|
|
7
7
|
export { KlantenDetailPage as KlantenDetailPagePlanner } from "./components/KlantenDetailPage";
|
|
8
|
+
export { KlantRisicoBanner, KlantRisicoBannerById, KlantRisicoBadge } from "./components/klanten/KlantRisicoBanner";
|
|
8
9
|
export { WerkbonDetailPage as WerkbonDetailPagePlanner } from "./components/WerkbonDetailPage";
|
|
9
10
|
export const klantenInstallatieTechniekPlannerModule = {
|
|
10
11
|
id: "installatietechniek-klanten-planner",
|
|
11
12
|
name: "Klanten planner",
|
|
12
|
-
version: "1.0.
|
|
13
|
+
version: "1.0.28",
|
|
13
14
|
nav: {
|
|
14
15
|
label: "Klanten",
|
|
15
16
|
href: "/installatietechniek-klanten-planner",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Geen project-URL of sleutel in module-code: de client komt van @flowselections/core (env-gebaseerd, dezelfde sessie als de shell).
|
|
2
2
|
import { supabase as coreSupabase } from "@flowselections/core";
|
|
3
3
|
// Import the supabase client like this:
|
|
4
|
-
// import { supabase } from "
|
|
4
|
+
// import { supabase } from "./client.js";
|
|
5
5
|
export const supabase = coreSupabase;
|
|
@@ -20,7 +20,7 @@ function createSupabaseAdminClient() {
|
|
|
20
20
|
let _supabaseAdmin;
|
|
21
21
|
// Server-side Supabase client with service role - bypasses RLS
|
|
22
22
|
// SECURITY: Only use this for trusted server-side operations, never expose to client code
|
|
23
|
-
// Import like: import { supabaseAdmin } from "
|
|
23
|
+
// Import like: import { supabaseAdmin } from "./client.server.js";
|
|
24
24
|
export const supabaseAdmin = new Proxy({}, {
|
|
25
25
|
get(_, prop, receiver) {
|
|
26
26
|
if (!_supabaseAdmin)
|