@flowselections/offertes-installatie-techniek 1.21.4 → 1.21.5

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.
@@ -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 "../../integrations/supabase/client";
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 "../../integrations/supabase/client.server";
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/offertes-installatie-techniek",
3
- "version": "1.21.4",
3
+ "version": "1.21.5",
4
4
  "flowselections": {
5
5
  "moduleId": "offertes-installatietechniek",
6
6
  "pages": [
@@ -152,7 +152,7 @@
152
152
  "devDependencies": {
153
153
  "@cloudflare/vite-plugin": "^1.25.5",
154
154
  "@eslint/js": "^9.32.0",
155
- "@flowselections/core": "1.0.29",
155
+ "@flowselections/core": "1.0.30",
156
156
  "@lovable.dev/vite-tanstack-config": "2.26.5",
157
157
  "@rsbuild/core": "^1.0.0",
158
158
  "@tailwindcss/vite": "^4.2.1",
@@ -1,6 +0,0 @@
1
- interface Props {
2
- offerteId: string;
3
- }
4
- export declare function OfferteDetailPage({ offerteId }: Props): import("react").JSX.Element;
5
- export {};
6
- //# sourceMappingURL=OfferteDetailPage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"OfferteDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/OfferteDetailPage.tsx"],"names":[],"mappings":"AAwBA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,+BAgPrD"}
@@ -1,48 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- // ============================================================================
3
- // src/components/OfferteDetailPage.tsx — detailpagina van één offerte
4
- // Geen eigen <Header /> — die levert de shell via staticData.title
5
- // ============================================================================
6
- import { useNavigate } from "@tanstack/react-router";
7
- import { Button, Card, CardContent, CardHeader, CardTitle, Separator, Textarea, cn, toast, } from "@flowselections/core";
8
- import { ArrowLeft, CheckCircle2, Send, ShieldCheck, XCircle } from "lucide-react";
9
- import { useOfferte } from "../hooks/useOfferte";
10
- import { useWerkbonPakketten } from "../hooks/useWerkbonPakketten";
11
- import { OfferteRegelsEditor } from "./offertes/OfferteRegelsEditor";
12
- import { OfferteStatusBadge } from "./offertes/OfferteStatusBadge";
13
- import { effectieveStatus, formatDatum, formatEuro } from "../lib/offertes";
14
- export function OfferteDetailPage({ offerteId }) {
15
- const navigate = useNavigate();
16
- const { offerte, klantNaam, werkbonNummer, regels, events, restantMandaat, loading, updateOfferte, addRegel, updateRegel, deleteRegel, naarWerkbon, } = useOfferte(offerteId);
17
- const { pakketten } = useWerkbonPakketten();
18
- if (loading) {
19
- return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Laden\u2026" });
20
- }
21
- if (!offerte) {
22
- return (_jsxs("main", { className: "space-y-4 p-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Deze offerte bestaat niet (meer)." }), _jsx(Button, { variant: "outline", onClick: () => navigate({ to: "/offertes" }), children: "Terug naar overzicht" })] }));
23
- }
24
- const status = effectieveStatus(offerte);
25
- const bewerkbaar = ["concept", "mandaat"].includes(status);
26
- const binnenMandaat = offerte.total_cents > 0 && offerte.total_cents <= restantMandaat;
27
- const isOmgezet = Boolean(offerte.follow_up_werkbon_id);
28
- const markeerMandaat = async () => {
29
- await updateOfferte({ status: "mandaat", binnen_mandaat: true, mandaat_restant_cents: restantMandaat }, "mandaat_bevestigd", "Binnen mandaat — geen verzending naar klant nodig");
30
- };
31
- const verzenden = () => updateOfferte({ status: "verzonden", sent_at: new Date().toISOString() }, "verzonden", "Offerte verzonden naar de klant");
32
- const goedkeuren = () => updateOfferte({ status: "goedgekeurd", approved_at: new Date().toISOString() }, "goedgekeurd", "Offerte goedgekeurd");
33
- const afwijzen = () => updateOfferte({ status: "afgewezen", rejected_at: new Date().toISOString() }, "afgewezen", "Offerte afgewezen");
34
- const omzetten = async () => {
35
- const ok = await naarWerkbon();
36
- if (ok)
37
- toast.success("Vervolgwerkbon staat klaar in de werkbonnenmodule");
38
- };
39
- return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-2", onClick: () => navigate({ to: "/offertes" }), children: [_jsx(ArrowLeft, { className: "mr-1 h-4 w-4" }), " Offertes"] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h1", { className: "font-mono text-2xl font-semibold text-foreground", children: offerte.offerte_number }), _jsx(OfferteStatusBadge, { status: status })] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [klantNaam ?? "Onbekende klant", werkbonNummer ? ` · vanuit ${werkbonNummer}` : ""] })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [bewerkbaar && !binnenMandaat && (_jsxs(Button, { variant: "outline", onClick: verzenden, children: [_jsx(Send, { className: "mr-1 h-4 w-4" }), " Verzenden"] })), bewerkbaar && binnenMandaat && status !== "mandaat" && (_jsxs(Button, { variant: "outline", onClick: markeerMandaat, children: [_jsx(ShieldCheck, { className: "mr-1 h-4 w-4" }), " Binnen mandaat"] })), ["verzonden", "bekeken"].includes(status) && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: afwijzen, children: [_jsx(XCircle, { className: "mr-1 h-4 w-4" }), " Afwijzen"] }), _jsxs(Button, { onClick: goedkeuren, children: [_jsx(CheckCircle2, { className: "mr-1 h-4 w-4" }), " Goedkeuren"] })] })), !isOmgezet && (status === "goedgekeurd" || (status === "mandaat" && binnenMandaat)) && (_jsx(Button, { onClick: omzetten, children: "Omzetten naar werkbon" }))] })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-3", children: [_jsxs("div", { className: "space-y-6 lg:col-span-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Offerteregels" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx(OfferteRegelsEditor, { regels: regels, pakketten: pakketten, readOnly: !bewerkbaar, onAdd: addRegel, onUpdate: updateRegel, onDelete: deleteRegel }), _jsx(Separator, {}), _jsxs("div", { className: "ml-auto w-full max-w-xs space-y-1 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Subtotaal" }), _jsx("span", { children: formatEuro(offerte.subtotal_cents) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Btw (", offerte.vat_rate, "%)"] }), _jsx("span", { children: formatEuro(offerte.vat_cents) })] }), _jsxs("div", { className: "flex justify-between text-base font-semibold", children: [_jsx("span", { children: "Totaal" }), _jsx("span", { children: formatEuro(offerte.total_cents) })] })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Omschrijving" }) }), _jsx(CardContent, { children: _jsx(Textarea, { rows: 4, defaultValue: offerte.omschrijving, disabled: !bewerkbaar, onBlur: (e) => e.target.value !== offerte.omschrijving &&
40
- updateOfferte({ omschrijving: e.target.value }) }) })] })] }), _jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Mandaat" }) }), _jsxs(CardContent, { className: "space-y-2 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Resterend mandaat" }), _jsx("span", { className: "font-medium", children: formatEuro(restantMandaat) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Offertetotaal" }), _jsx("span", { className: "font-medium", children: formatEuro(offerte.total_cents) })] }), _jsx("p", { className: cn("rounded-md px-3 py-2 text-sm", binnenMandaat
41
- ? "bg-primary/10 text-foreground"
42
- : "bg-muted text-muted-foreground"), children: binnenMandaat
43
- ? "Binnen mandaat — deze offerte mag direct worden omgezet in een werkbon zonder verzending naar de klant."
44
- : "Buiten mandaat — de klant moet deze offerte eerst goedkeuren." }), isOmgezet && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Vervolgwerkbon aangemaakt op ", formatDatum(offerte.converted_at), "."] }))] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Gegevens" }) }), _jsxs(CardContent, { className: "space-y-2 text-sm", children: [_jsx(Rij, { label: "Geldig t/m", value: formatDatum(offerte.valid_until) }), _jsx(Rij, { label: "Aangemaakt", value: formatDatum(offerte.created_at) }), _jsx(Rij, { label: "Verzonden", value: formatDatum(offerte.sent_at) }), _jsx(Rij, { label: "Goedgekeurd", value: formatDatum(offerte.approved_at) })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Activiteit" }) }), _jsxs(CardContent, { className: "space-y-3", children: [(Array.isArray(events) ? events : []).length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit." })), (Array.isArray(events) ? events : []).map((event) => (_jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-medium", children: event.omschrijving || event.event_type }), _jsx("p", { className: "text-xs text-muted-foreground", children: formatDatum(event.created_at) })] }, event.id)))] })] })] })] })] }));
45
- }
46
- function Rij({ label, value }) {
47
- return (_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { children: value })] }));
48
- }
@@ -1,2 +0,0 @@
1
- export declare function OffertesOverzichtPage(): import("react").JSX.Element;
2
- //# sourceMappingURL=OffertesOverzichtPage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"OffertesOverzichtPage.d.ts","sourceRoot":"","sources":["../../src/components/OffertesOverzichtPage.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,gCAiJpC"}
@@ -1,40 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // ============================================================================
3
- // src/components/OffertesOverzichtPage.tsx — offerteoverzicht
4
- // Geen eigen <Header /> — die levert de shell via staticData.title
5
- // ============================================================================
6
- import { useMemo, useState } from "react";
7
- import { useNavigate } from "@tanstack/react-router";
8
- import { Button, Card, CardContent, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsList, TabsTrigger, cn, } from "@flowselections/core";
9
- import { Building2, Plus, Search } from "lucide-react";
10
- import { useOffertes } from "../hooks/useOffertes";
11
- import { OfferteKpiCards } from "./offertes/OfferteKpiCards";
12
- import { OfferteStatusBadge } from "./offertes/OfferteStatusBadge";
13
- import { NieuweOfferteDialog } from "./offertes/NieuweOfferteDialog";
14
- import { OFFERTE_TABS, effectieveStatus, formatDatum, formatEuro } from "../lib/offertes";
15
- export function OffertesOverzichtPage() {
16
- const { offertes, loading, reload } = useOffertes();
17
- const navigate = useNavigate();
18
- const [tab, setTab] = useState("alle");
19
- const [zoek, setZoek] = useState("");
20
- const [dialogOpen, setDialogOpen] = useState(false);
21
- const zichtbaar = useMemo(() => {
22
- const lijst = Array.isArray(offertes) ? offertes : [];
23
- const tabDef = OFFERTE_TABS.find((t) => t.key === tab);
24
- const term = zoek.trim().toLowerCase();
25
- return lijst.filter((o) => {
26
- const status = effectieveStatus(o);
27
- if (tabDef?.statuses && !tabDef.statuses.includes(status))
28
- return false;
29
- if (!term)
30
- return true;
31
- return [o.offerte_number, o.klant_naam, o.omschrijving, o.werkbon_nummer]
32
- .filter(Boolean)
33
- .some((v) => String(v).toLowerCase().includes(term));
34
- });
35
- }, [offertes, tab, zoek]);
36
- return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-foreground", children: "Offertes" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Offertes vanuit werkbonnen, klantgoedkeuring en vervolgwerk" })] }), _jsxs(Button, { onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Nieuwe offerte"] })] }), _jsx(OfferteKpiCards, { offertes: offertes }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx(Tabs, { value: tab, onValueChange: setTab, children: _jsx(TabsList, { children: OFFERTE_TABS.map((t) => (_jsx(TabsTrigger, { value: t.key, children: t.label }, t.key))) }) }), _jsxs("div", { className: "relative w-full max-w-xs", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: "Zoek op klant, nummer, omschrijving", value: zoek, onChange: (e) => setZoek(e.target.value) })] })] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-36", children: "Nr." }), _jsx(TableHead, { children: "Klant" }), _jsx(TableHead, { children: "Omschrijving" }), _jsx(TableHead, { className: "text-right", children: "Totaal" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Geldig t/m" }), _jsx(TableHead, { children: "Aangemaakt" })] }) }), _jsxs(TableBody, { children: [loading && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 7, className: "text-sm text-muted-foreground", children: "Laden\u2026" }) })), !loading && zichtbaar.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 7, className: "text-sm text-muted-foreground", children: "Geen offertes gevonden." }) })), zichtbaar.map((o) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => navigate({ to: "/offertes/$offerteId", params: { offerteId: o.id } }), children: [_jsx(TableCell, { className: "font-mono text-xs font-semibold", children: o.offerte_number }), _jsx(TableCell, { children: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-primary/10", children: _jsx(Building2, { className: "h-3.5 w-3.5 text-primary" }) }), _jsx("span", { className: "font-medium", children: o.klant_naam ?? "—" })] }) }), _jsx(TableCell, { className: "max-w-xs truncate text-muted-foreground", children: o.omschrijving || "—" }), _jsx(TableCell, { className: "text-right font-medium", children: formatEuro(o.total_cents) }), _jsx(TableCell, { children: _jsx(OfferteStatusBadge, { status: effectieveStatus(o) }) }), _jsx(TableCell, { className: cn("text-sm", !o.valid_until && "text-muted-foreground"), children: formatDatum(o.valid_until) }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: formatDatum(o.created_at) })] }, o.id)))] })] }) }) }), _jsx(NieuweOfferteDialog, { open: dialogOpen, onOpenChange: setDialogOpen, onCreated: (id) => {
37
- reload();
38
- navigate({ to: "/offertes/$offerteId", params: { offerteId: id } });
39
- } }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Werkbonnen, monteurnotities en pakketten worden beheerd in de module Werkbonnen." })] }));
40
- }
@@ -1,8 +0,0 @@
1
- interface Props {
2
- open: boolean;
3
- onOpenChange: (open: boolean) => void;
4
- onCreated: (offerteId: string) => void;
5
- }
6
- export declare function NieuweOfferteDialog({ open, onOpenChange, onCreated }: Props): import("react").JSX.Element;
7
- export {};
8
- //# sourceMappingURL=NieuweOfferteDialog.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"NieuweOfferteDialog.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/NieuweOfferteDialog.tsx"],"names":[],"mappings":"AAsBA,UAAU,KAAK;IACb,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAsBD,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,KAAK,+BAkH3E"}
@@ -1,81 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { supabase, toast, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@flowselections/core";
4
- import { formatEuro } from "../../lib/offertes";
5
- const db = supabase;
6
- const AFGEROND_STATUSSEN = ["completed", "invoicing"];
7
- function bronStatusLabel(status) {
8
- switch (status) {
9
- case "completed":
10
- return "Gereed";
11
- case "invoicing":
12
- return "Te factureren";
13
- case "in_progress":
14
- return "In behandeling";
15
- case "planned_visible":
16
- case "planned_hidden":
17
- return "Gepland";
18
- case "unassigned":
19
- return "Niet toegewezen";
20
- default:
21
- return status ?? "Onbekend";
22
- }
23
- }
24
- export function NieuweOfferteDialog({ open, onOpenChange, onCreated }) {
25
- const [bronnen, setBronnen] = useState([]);
26
- const [bronId, setBronId] = useState("");
27
- const [omschrijving, setOmschrijving] = useState("");
28
- const [saving, setSaving] = useState(false);
29
- useEffect(() => {
30
- if (!open)
31
- return;
32
- (async () => {
33
- const { data } = await db.rpc("quote_list_sources", {
34
- _source_type: "work_order",
35
- _limit: 100,
36
- });
37
- const rows = Array.isArray(data) ? data : [];
38
- const sorted = [...rows].sort((a, b) => {
39
- const aDone = AFGEROND_STATUSSEN.includes(a.status ?? "") ? 1 : 0;
40
- const bDone = AFGEROND_STATUSSEN.includes(b.status ?? "") ? 1 : 0;
41
- return bDone - aDone;
42
- });
43
- setBronnen(sorted);
44
- })();
45
- }, [open]);
46
- const geselecteerd = bronnen.find((w) => w.id === bronId) ?? null;
47
- useEffect(() => {
48
- if (!geselecteerd)
49
- return;
50
- const bron = geselecteerd.description?.trim() ?? "";
51
- setOmschrijving(bron ? `Vervolgwerk n.a.v. werkbon: ${bron}` : "");
52
- }, [bronId]);
53
- const maakOfferte = async () => {
54
- if (!geselecteerd) {
55
- toast.error("Kies eerst een werkbon");
56
- return;
57
- }
58
- setSaving(true);
59
- try {
60
- const { data, error } = await db.rpc("quote_create_from_source", {
61
- _source_type: "work_order",
62
- _source_id: geselecteerd.id,
63
- _payload: { omschrijving },
64
- });
65
- if (error || !data)
66
- throw error ?? new Error("geen id");
67
- toast.success("Offerte aangemaakt");
68
- onOpenChange(false);
69
- setBronId("");
70
- setOmschrijving("");
71
- onCreated(data);
72
- }
73
- catch {
74
- toast.error("Offerte aanmaken mislukt");
75
- }
76
- finally {
77
- setSaving(false);
78
- }
79
- };
80
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Nieuwe offerte vanuit werkbon" }) }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "werkbon", children: "Werkbon" }), _jsxs(Select, { value: bronId, onValueChange: setBronId, children: [_jsx(SelectTrigger, { id: "werkbon", children: _jsx(SelectValue, { placeholder: "Kies een werkbon" }) }), _jsx(SelectContent, { className: "max-h-72", children: bronnen.map((w) => (_jsxs(SelectItem, { value: w.id, children: [_jsx("span", { className: "font-medium", children: w.number }), _jsx("span", { className: "mx-2 text-muted-foreground", children: "\u00B7" }), _jsx("span", { className: "text-muted-foreground", children: bronStatusLabel(w.status) }), _jsx("span", { className: "mx-2 text-muted-foreground", children: "\u00B7" }), (w.description ?? "").slice(0, 40)] }, w.id))) })] })] }), geselecteerd && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Mandaat op deze werkbon: ", formatEuro(geselecteerd.budget_mandate_cents ?? 0)] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "omschrijving", children: "Omschrijving" }), _jsx(Textarea, { id: "omschrijving", rows: 4, value: omschrijving, onChange: (e) => setOmschrijving(e.target.value), placeholder: "Wat wordt er aangeboden?" })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { onClick: maakOfferte, disabled: saving || !bronId, children: "Offerte aanmaken" })] })] }) }));
81
- }
@@ -1,7 +0,0 @@
1
- import { type OfferteMetKlant } from "../../lib/offertes";
2
- interface Props {
3
- offertes: OfferteMetKlant[];
4
- }
5
- export declare function OfferteKpiCards({ offertes }: Props): import("react").JSX.Element;
6
- export {};
7
- //# sourceMappingURL=OfferteKpiCards.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"OfferteKpiCards.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteKpiCards.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAE5B,UAAU,KAAK;IACb,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,+BAwDlD"}
@@ -1,33 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Card, CardContent } from "@flowselections/core";
3
- import { CheckCircle2, Clock, Euro, TrendingUp } from "lucide-react";
4
- import { formatEuro, effectieveStatus, OPEN_STATUSES, } from "../../lib/offertes";
5
- export function OfferteKpiCards({ offertes }) {
6
- const veilig = Array.isArray(offertes) ? offertes : [];
7
- const openstaand = veilig
8
- .filter((o) => OPEN_STATUSES.includes(effectieveStatus(o)))
9
- .reduce((sum, o) => sum + (o.total_cents ?? 0), 0);
10
- const besloten = veilig.filter((o) => ["goedgekeurd", "omgezet", "afgewezen"].includes(effectieveStatus(o)));
11
- const goedgekeurd = veilig.filter((o) => ["goedgekeurd", "omgezet"].includes(effectieveStatus(o)));
12
- const conversie = besloten.length
13
- ? Math.round((goedgekeurd.length / besloten.length) * 100)
14
- : 0;
15
- const doorlooptijden = goedgekeurd
16
- .filter((o) => o.approved_at)
17
- .map((o) => (new Date(o.approved_at).getTime() - new Date(o.created_at).getTime()) /
18
- 86400000);
19
- const gemiddeld = doorlooptijden.length
20
- ? doorlooptijden.reduce((a, b) => a + b, 0) / doorlooptijden.length
21
- : 0;
22
- const items = [
23
- { icon: Euro, label: "Openstaand bedrag", value: formatEuro(openstaand) },
24
- { icon: TrendingUp, label: "Conversie", value: `${conversie}%` },
25
- { icon: Clock, label: "Gem. doorlooptijd", value: `${gemiddeld.toFixed(1)} dgn` },
26
- {
27
- icon: CheckCircle2,
28
- label: "Goedgekeurd",
29
- value: `${goedgekeurd.length} / ${veilig.length}`,
30
- },
31
- ];
32
- return (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: items.map(({ icon: Icon, label, value }) => (_jsx(Card, { children: _jsxs(CardContent, { className: "flex items-center gap-3 p-4", children: [_jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted", children: _jsx(Icon, { className: "h-5 w-5 text-primary" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-xs text-muted-foreground", children: label }), _jsx("p", { className: "text-xl font-semibold text-foreground", children: value })] })] }) }, label))) }));
33
- }