@flowselections/offertes-installatie-techniek 1.0.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.
Files changed (73) hide show
  1. package/README.md +24 -0
  2. package/dist-lib/_core-safelist.d.ts +2 -0
  3. package/dist-lib/_core-safelist.d.ts.map +1 -0
  4. package/dist-lib/_core-safelist.js +15 -0
  5. package/dist-lib/components/OfferteDetailPage.d.ts +6 -0
  6. package/dist-lib/components/OfferteDetailPage.d.ts.map +1 -0
  7. package/dist-lib/components/OfferteDetailPage.js +48 -0
  8. package/dist-lib/components/OffertesOverzichtPage.d.ts +2 -0
  9. package/dist-lib/components/OffertesOverzichtPage.d.ts.map +1 -0
  10. package/dist-lib/components/OffertesOverzichtPage.js +40 -0
  11. package/dist-lib/components/offertes/NieuweOfferteDialog.d.ts +8 -0
  12. package/dist-lib/components/offertes/NieuweOfferteDialog.d.ts.map +1 -0
  13. package/dist-lib/components/offertes/NieuweOfferteDialog.js +81 -0
  14. package/dist-lib/components/offertes/OfferteDetailPage.d.ts +6 -0
  15. package/dist-lib/components/offertes/OfferteDetailPage.d.ts.map +1 -0
  16. package/dist-lib/components/offertes/OfferteDetailPage.js +48 -0
  17. package/dist-lib/components/offertes/OfferteDetailRoute.d.ts +2 -0
  18. package/dist-lib/components/offertes/OfferteDetailRoute.d.ts.map +1 -0
  19. package/dist-lib/components/offertes/OfferteDetailRoute.js +13 -0
  20. package/dist-lib/components/offertes/OfferteKpiCards.d.ts +7 -0
  21. package/dist-lib/components/offertes/OfferteKpiCards.d.ts.map +1 -0
  22. package/dist-lib/components/offertes/OfferteKpiCards.js +33 -0
  23. package/dist-lib/components/offertes/OfferteRegelsEditor.d.ts +12 -0
  24. package/dist-lib/components/offertes/OfferteRegelsEditor.d.ts.map +1 -0
  25. package/dist-lib/components/offertes/OfferteRegelsEditor.js +27 -0
  26. package/dist-lib/components/offertes/OfferteStatusBadge.d.ts +4 -0
  27. package/dist-lib/components/offertes/OfferteStatusBadge.d.ts.map +1 -0
  28. package/dist-lib/components/offertes/OfferteStatusBadge.js +16 -0
  29. package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts +2 -0
  30. package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts.map +1 -0
  31. package/dist-lib/components/offertes/OffertesOverzichtPage.js +40 -0
  32. package/dist-lib/hooks/useOfferte.d.ts +17 -0
  33. package/dist-lib/hooks/useOfferte.d.ts.map +1 -0
  34. package/dist-lib/hooks/useOfferte.js +135 -0
  35. package/dist-lib/hooks/useOffertes.d.ts +7 -0
  36. package/dist-lib/hooks/useOffertes.d.ts.map +1 -0
  37. package/dist-lib/hooks/useOffertes.js +38 -0
  38. package/dist-lib/hooks/useWerkbonPakketten.d.ts +7 -0
  39. package/dist-lib/hooks/useWerkbonPakketten.d.ts.map +1 -0
  40. package/dist-lib/hooks/useWerkbonPakketten.js +20 -0
  41. package/dist-lib/index.d.ts +15 -0
  42. package/dist-lib/index.d.ts.map +1 -0
  43. package/dist-lib/index.js +32 -0
  44. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  45. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  46. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  47. package/dist-lib/integrations/supabase/auth-middleware.d.ts +23409 -0
  48. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  49. package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
  50. package/dist-lib/integrations/supabase/client.d.ts +23405 -0
  51. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  52. package/dist-lib/integrations/supabase/client.js +14 -0
  53. package/dist-lib/integrations/supabase/client.server.d.ts +23405 -0
  54. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  55. package/dist-lib/integrations/supabase/client.server.js +30 -0
  56. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
  57. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
  58. package/dist-lib/integrations/supabase/previewAuthStorage.js +92 -0
  59. package/dist-lib/integrations/supabase/types.d.ts +24034 -0
  60. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  61. package/dist-lib/integrations/supabase/types.js +92 -0
  62. package/dist-lib/lib/offertes.d.ts +86 -0
  63. package/dist-lib/lib/offertes.d.ts.map +1 -0
  64. package/dist-lib/lib/offertes.js +51 -0
  65. package/dist-lib/lib/utils.d.ts +3 -0
  66. package/dist-lib/lib/utils.d.ts.map +1 -0
  67. package/dist-lib/lib/utils.js +5 -0
  68. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  69. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  70. package/dist-lib/lib/validationSchemas.js +25 -0
  71. package/dist-lib/styles.css +1 -0
  72. package/package.json +92 -0
  73. package/public/logo.png +0 -0
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Offertes - Installatie Techniek
2
+
3
+ blank project start
4
+
5
+ This project was built with [Lovable](https://lovable.dev).
6
+
7
+ ## Build with Lovable
8
+
9
+ Continue developing this project in the [Lovable editor](https://lovable.dev/projects/a26e8dec-7784-4e34-b97e-8cc0cd056ea5).
10
+
11
+ - **Ship faster**: describe what you want to build and Lovable handles the code.
12
+ - **Stay in sync**: every change made in Lovable is committed straight to this repository.
13
+ - **Full ownership**: this code is yours. Push to `main` on GitHub and your changes sync back into Lovable, ready for your next prompt.
14
+
15
+ ## Development
16
+
17
+ Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
18
+
19
+ ```sh
20
+ git clone <this-repository-url>
21
+ cd <repository-name>
22
+ npm i
23
+ npm run dev
24
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_core-safelist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ // Forceert Tailwind classes die uit @flowselections/core komen maar niet
2
+ // voorkomen in de eigen broncode van deze module.
3
+ // Zonder dit bestand worden deze classes weggeoptimaliseerd op de
4
+ // Lovable productie build.
5
+ const _safelist = [
6
+ // Layout — Sidebar en shell structuur
7
+ 'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
8
+ 'items-start', 'items-center', 'justify-between', 'border-t',
9
+ 'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
10
+ // Grid — InstellingenPage
11
+ 'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
12
+ // Toggle/Switch — aan/uit zichtbaarheid
13
+ 'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
14
+ ];
15
+ export {};
@@ -0,0 +1,6 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function OffertesOverzichtPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=OffertesOverzichtPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OffertesOverzichtPage.d.ts","sourceRoot":"","sources":["../../src/components/OffertesOverzichtPage.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,gCAiJpC"}
@@ -0,0 +1,40 @@
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
+ }
@@ -0,0 +1,8 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,81 @@
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
+ }
@@ -0,0 +1,6 @@
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteDetailPage.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteDetailPage.tsx"],"names":[],"mappings":"AAwBA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,+BAgPrD"}
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // ============================================================================
3
+ // src/components/offertes/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 "./OfferteRegelsEditor";
12
+ import { OfferteStatusBadge } from "./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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function OfferteDetailRoute(): import("react").JSX.Element;
2
+ //# sourceMappingURL=OfferteDetailRoute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteDetailRoute.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteDetailRoute.tsx"],"names":[],"mappings":"AASA,wBAAgB,kBAAkB,gCAKjC"}
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // ============================================================================
3
+ // src/components/offertes/OfferteDetailRoute.tsx
4
+ // Route-component voor /offertes/$offerteId — haalt de param op en rendert
5
+ // de detailpagina. Geen eigen <Header /> — die levert de shell.
6
+ // ============================================================================
7
+ import { useParams } from "@tanstack/react-router";
8
+ import { OfferteDetailPage } from "./OfferteDetailPage";
9
+ export function OfferteDetailRoute() {
10
+ const params = useParams({ strict: false });
11
+ const offerteId = params.offerteId ?? "";
12
+ return _jsx(OfferteDetailPage, { offerteId: offerteId });
13
+ }
@@ -0,0 +1,7 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,12 @@
1
+ import { type OfferteRegel, type Pakket } from "../../lib/offertes";
2
+ interface Props {
3
+ regels: OfferteRegel[];
4
+ pakketten: Pakket[];
5
+ readOnly: boolean;
6
+ onAdd: (regel: Partial<OfferteRegel>) => void;
7
+ onUpdate: (id: string, patch: Partial<OfferteRegel>) => void;
8
+ onDelete: (id: string) => void;
9
+ }
10
+ export declare function OfferteRegelsEditor({ regels, pakketten, readOnly, onAdd, onUpdate, onDelete, }: Props): import("react").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=OfferteRegelsEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteRegelsEditor.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteRegelsEditor.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEhF,UAAU,KAAK;IACb,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IAC7D,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,SAAS,EACT,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,KAAK,+BA+HP"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
4
+ import { Plus, Trash2 } from "lucide-react";
5
+ import { formatEuro } from "../../lib/offertes";
6
+ export function OfferteRegelsEditor({ regels, pakketten, readOnly, onAdd, onUpdate, onDelete, }) {
7
+ const [pakketId, setPakketId] = useState("");
8
+ const veiligeRegels = Array.isArray(regels) ? regels : [];
9
+ const veiligePakketten = Array.isArray(pakketten) ? pakketten : [];
10
+ const voegPakketToe = () => {
11
+ const pakket = veiligePakketten.find((p) => p.id === pakketId);
12
+ if (!pakket)
13
+ return;
14
+ onAdd({
15
+ package_id: pakket.id,
16
+ source: "pakket",
17
+ omschrijving: pakket.name,
18
+ quantity: pakket.default_quantity ?? 1,
19
+ unit_price_cents: pakket.unit_price_cents ?? 0,
20
+ });
21
+ setPakketId("");
22
+ };
23
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Omschrijving" }), _jsx(TableHead, { className: "w-24 text-right", children: "Aantal" }), _jsx(TableHead, { className: "w-32 text-right", children: "Stukprijs" }), _jsx(TableHead, { className: "w-32 text-right", children: "Totaal" }), !readOnly && _jsx(TableHead, { className: "w-12" })] }) }), _jsxs(TableBody, { children: [veiligeRegels.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: readOnly ? 4 : 5, className: "text-sm text-muted-foreground", children: "Nog geen regels toegevoegd." }) })), veiligeRegels.map((regel) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: readOnly ? (_jsx("span", { className: "text-sm", children: regel.omschrijving })) : (_jsx(Input, { defaultValue: regel.omschrijving, onBlur: (e) => e.target.value !== regel.omschrijving &&
24
+ onUpdate(regel.id, { omschrijving: e.target.value }) })) }), _jsx(TableCell, { className: "text-right", children: readOnly ? (Number(regel.quantity)) : (_jsx(Input, { type: "number", step: "0.25", className: "text-right", defaultValue: Number(regel.quantity), onBlur: (e) => onUpdate(regel.id, { quantity: Number(e.target.value) || 0 }) })) }), _jsx(TableCell, { className: "text-right", children: readOnly ? (formatEuro(regel.unit_price_cents)) : (_jsx(Input, { type: "number", step: "0.01", className: "text-right", defaultValue: (regel.unit_price_cents / 100).toFixed(2), onBlur: (e) => onUpdate(regel.id, {
25
+ unit_price_cents: Math.round((Number(e.target.value) || 0) * 100),
26
+ }) })) }), _jsx(TableCell, { className: "text-right font-medium", children: formatEuro(regel.line_total_cents) }), !readOnly && (_jsx(TableCell, { children: _jsx(Button, { variant: "ghost", size: "icon", onClick: () => onDelete(regel.id), children: _jsx(Trash2, { className: "h-4 w-4" }) }) }))] }, regel.id)))] })] }), !readOnly && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Select, { value: pakketId, onValueChange: setPakketId, children: [_jsx(SelectTrigger, { className: "w-72", children: _jsx(SelectValue, { placeholder: "Pakket toevoegen" }) }), _jsx(SelectContent, { children: veiligePakketten.map((p) => (_jsxs(SelectItem, { value: p.id, children: [p.name, " \u2014 ", formatEuro(p.unit_price_cents ?? 0)] }, p.id))) })] }), _jsxs(Button, { variant: "secondary", onClick: voegPakketToe, disabled: !pakketId, children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Pakket"] }), _jsxs(Button, { variant: "outline", onClick: () => onAdd({ source: "vrij", omschrijving: "Nieuwe regel", quantity: 1, unit_price_cents: 0 }), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Vrije regel"] })] }))] }));
27
+ }
@@ -0,0 +1,4 @@
1
+ export declare function OfferteStatusBadge({ status }: {
2
+ status: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=OfferteStatusBadge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteStatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteStatusBadge.tsx"],"names":[],"mappings":"AAcA,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,+BAShE"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Badge, cn } from "@flowselections/core";
3
+ import { OFFERTE_STATUS_LABELS } from "../../lib/offertes";
4
+ const STYLES = {
5
+ concept: "bg-muted text-muted-foreground border-transparent",
6
+ mandaat: "bg-accent text-accent-foreground border-transparent",
7
+ verzonden: "bg-secondary text-secondary-foreground border-transparent",
8
+ bekeken: "bg-secondary text-secondary-foreground border-transparent",
9
+ goedgekeurd: "bg-primary text-primary-foreground border-transparent",
10
+ omgezet: "bg-primary text-primary-foreground border-transparent",
11
+ afgewezen: "bg-destructive text-destructive-foreground border-transparent",
12
+ verlopen: "bg-background text-muted-foreground border-border",
13
+ };
14
+ export function OfferteStatusBadge({ status }) {
15
+ return (_jsx(Badge, { variant: "outline", className: cn("rounded-full px-3 font-medium", STYLES[status] ?? STYLES.concept), children: OFFERTE_STATUS_LABELS[status] ?? status }));
16
+ }
@@ -0,0 +1,2 @@
1
+ export declare function OffertesOverzichtPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=OffertesOverzichtPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OffertesOverzichtPage.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OffertesOverzichtPage.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,gCAiJpC"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // ============================================================================
3
+ // src/components/offertes/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 "./OfferteKpiCards";
12
+ import { OfferteStatusBadge } from "./OfferteStatusBadge";
13
+ import { NieuweOfferteDialog } from "./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
+ }
@@ -0,0 +1,17 @@
1
+ import type { Offerte, OfferteEvent, OfferteRegel } from "../lib/offertes";
2
+ export declare function useOfferte(offerteId: string): {
3
+ offerte: Offerte | null;
4
+ klantNaam: string | null;
5
+ werkbonNummer: string | null;
6
+ regels: OfferteRegel[];
7
+ events: OfferteEvent[];
8
+ restantMandaat: number;
9
+ loading: boolean;
10
+ reload: () => Promise<void>;
11
+ updateOfferte: (patch: Record<string, unknown>, eventType?: string, eventTekst?: string) => Promise<boolean>;
12
+ addRegel: (regel: Partial<OfferteRegel>) => Promise<void>;
13
+ updateRegel: (id: string, patch: Partial<OfferteRegel>) => Promise<void>;
14
+ deleteRegel: (id: string) => Promise<void>;
15
+ naarWerkbon: () => Promise<boolean>;
16
+ };
17
+ //# sourceMappingURL=useOfferte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOfferte.d.ts","sourceRoot":"","sources":["../../src/hooks/useOfferte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAI3E,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM;;;;;;;;;2BAuE1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,MAAM,eAAe,MAAM;sBAchE,OAAO,CAAC,YAAY,CAAC;sBAgBxB,MAAM,SAAS,OAAO,CAAC,YAAY,CAAC;sBAYpC,MAAM;;EAqCpB"}