@flowselections/offertes-installatie-techniek 1.0.3 → 1.21.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 (108) hide show
  1. package/README.md +16 -1
  2. package/dist-lib/components/offertes/AkkoordRoute.d.ts +2 -0
  3. package/dist-lib/components/offertes/AkkoordRoute.d.ts.map +1 -0
  4. package/dist-lib/components/offertes/AkkoordRoute.js +11 -0
  5. package/dist-lib/components/offertes/ArtikelKiezer.d.ts +16 -0
  6. package/dist-lib/components/offertes/ArtikelKiezer.d.ts.map +1 -0
  7. package/dist-lib/components/offertes/ArtikelKiezer.js +56 -0
  8. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts +4 -0
  9. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts.map +1 -0
  10. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.js +42 -0
  11. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts +14 -0
  12. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts.map +1 -0
  13. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.js +91 -0
  14. package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts +5 -0
  15. package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts.map +1 -0
  16. package/dist-lib/components/offertes/MoederWerkbonPaneel.js +66 -0
  17. package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts +33 -0
  18. package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts.map +1 -0
  19. package/dist-lib/components/offertes/NieuweOfferteFormulier.js +523 -0
  20. package/dist-lib/components/offertes/OfferteA4.d.ts +28 -0
  21. package/dist-lib/components/offertes/OfferteA4.d.ts.map +1 -0
  22. package/dist-lib/components/offertes/OfferteA4.js +47 -0
  23. package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts +4 -0
  24. package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts.map +1 -0
  25. package/dist-lib/components/offertes/OfferteAkkoordPage.js +66 -0
  26. package/dist-lib/components/offertes/OfferteDetailPage.d.ts.map +1 -1
  27. package/dist-lib/components/offertes/OfferteDetailPage.js +247 -23
  28. package/dist-lib/components/offertes/OfferteRegelsEditor.js +1 -1
  29. package/dist-lib/components/offertes/OfferteStatusBadge.d.ts.map +1 -1
  30. package/dist-lib/components/offertes/OfferteStatusBadge.js +1 -0
  31. package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts.map +1 -1
  32. package/dist-lib/components/offertes/OffertesOverzichtPage.js +25 -3
  33. package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts +14 -0
  34. package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts.map +1 -0
  35. package/dist-lib/components/offertes/VoorstelOffertePaneel.js +144 -0
  36. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts +32 -0
  37. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts.map +1 -0
  38. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.js +26 -0
  39. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts +4 -0
  40. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts.map +1 -0
  41. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.js +47 -0
  42. package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts +2 -0
  43. package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts.map +1 -0
  44. package/dist-lib/components/settings/OfferteEmailSettingsCard.js +38 -0
  45. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts +2 -0
  46. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts.map +1 -0
  47. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.js +66 -0
  48. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts +2 -0
  49. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts.map +1 -0
  50. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.js +30 -0
  51. package/dist-lib/hooks/useExtraWerkMeldingen.d.ts +32 -0
  52. package/dist-lib/hooks/useExtraWerkMeldingen.d.ts.map +1 -0
  53. package/dist-lib/hooks/useExtraWerkMeldingen.js +103 -0
  54. package/dist-lib/hooks/useHuisstijl.d.ts +32 -0
  55. package/dist-lib/hooks/useHuisstijl.d.ts.map +1 -0
  56. package/dist-lib/hooks/useHuisstijl.js +60 -0
  57. package/dist-lib/hooks/useOfferte.d.ts +21 -0
  58. package/dist-lib/hooks/useOfferte.d.ts.map +1 -1
  59. package/dist-lib/hooks/useOfferte.js +105 -4
  60. package/dist-lib/hooks/useOffertes.d.ts.map +1 -1
  61. package/dist-lib/hooks/useOffertes.js +17 -7
  62. package/dist-lib/hooks/useWerkbonVoorstellen.d.ts +45 -0
  63. package/dist-lib/hooks/useWerkbonVoorstellen.d.ts.map +1 -0
  64. package/dist-lib/hooks/useWerkbonVoorstellen.js +74 -0
  65. package/dist-lib/index.d.ts +20 -2
  66. package/dist-lib/index.d.ts.map +1 -1
  67. package/dist-lib/index.js +27 -4
  68. package/dist-lib/integrations/supabase/auth-middleware.d.ts +4 -23403
  69. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  70. package/dist-lib/integrations/supabase/auth-middleware.js +1 -1
  71. package/dist-lib/integrations/supabase/client.d.ts +2 -23403
  72. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  73. package/dist-lib/integrations/supabase/client.server.d.ts +2 -23403
  74. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  75. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
  76. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  77. package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
  78. package/dist-lib/integrations/supabase/types.d.ts +23504 -11091
  79. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  80. package/dist-lib/integrations/supabase/types.js +2 -0
  81. package/dist-lib/lib/artikelenZoek.d.ts +17 -0
  82. package/dist-lib/lib/artikelenZoek.d.ts.map +1 -0
  83. package/dist-lib/lib/artikelenZoek.js +37 -0
  84. package/dist-lib/lib/offerteAcceptatie.functions.d.ts +7 -0
  85. package/dist-lib/lib/offerteAcceptatie.functions.d.ts.map +1 -0
  86. package/dist-lib/lib/offerteAcceptatie.functions.js +28 -0
  87. package/dist-lib/lib/offerteAdres.d.ts +7 -0
  88. package/dist-lib/lib/offerteAdres.d.ts.map +1 -0
  89. package/dist-lib/lib/offerteAdres.js +19 -0
  90. package/dist-lib/lib/offerteAi.functions.d.ts +34 -0
  91. package/dist-lib/lib/offerteAi.functions.d.ts.map +1 -0
  92. package/dist-lib/lib/offerteAi.functions.js +252 -0
  93. package/dist-lib/lib/offerteMail.d.ts +27 -0
  94. package/dist-lib/lib/offerteMail.d.ts.map +1 -0
  95. package/dist-lib/lib/offerteMail.js +141 -0
  96. package/dist-lib/lib/offerteRegels.d.ts +9 -0
  97. package/dist-lib/lib/offerteRegels.d.ts.map +1 -0
  98. package/dist-lib/lib/offerteRegels.js +20 -0
  99. package/dist-lib/lib/offerteRegels.test.d.ts +2 -0
  100. package/dist-lib/lib/offerteRegels.test.d.ts.map +1 -0
  101. package/dist-lib/lib/offerteRegels.test.js +17 -0
  102. package/dist-lib/lib/offerteReistijd.functions.d.ts +22 -0
  103. package/dist-lib/lib/offerteReistijd.functions.d.ts.map +1 -0
  104. package/dist-lib/lib/offerteReistijd.functions.js +173 -0
  105. package/dist-lib/lib/offertes.d.ts +8 -1
  106. package/dist-lib/lib/offertes.d.ts.map +1 -1
  107. package/dist-lib/lib/offertes.js +6 -4
  108. package/package.json +97 -5
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Offerte als A4-vellen in de huisstijl (gedeeld met Facturen).
3
+ // editable = alle teksten direct op het vel aanpasbaar, zoals bij de factuur.
4
+ import { forwardRef, useEffect, useRef } from "react";
5
+ import { Plus, Trash2 } from "lucide-react";
6
+ import { formatEuro } from "../../lib/offertes";
7
+ import { useHuisstijl } from "../../hooks/useHuisstijl";
8
+ const invoer = "w-full rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 outline-none focus:border-primary";
9
+ function datum(v) {
10
+ if (!v)
11
+ return "—";
12
+ return new Intl.DateTimeFormat("nl-NL", { day: "2-digit", month: "2-digit", year: "numeric" }).format(new Date(v));
13
+ }
14
+ function AutoTekst({ value, onChange, placeholder, className }) {
15
+ const ref = useRef(null);
16
+ useEffect(() => {
17
+ const el = ref.current;
18
+ if (!el)
19
+ return;
20
+ el.style.height = "auto";
21
+ el.style.height = `${el.scrollHeight}px`;
22
+ }, [value]);
23
+ return (_jsx("textarea", { ref: ref, rows: 1, value: value, placeholder: placeholder, onChange: (e) => onChange(e.target.value), className: `${invoer} resize-none overflow-hidden ${className ?? ""}` }));
24
+ }
25
+ function Briefhoofd({ h, logoUrl }) {
26
+ const contact = h
27
+ ? [h.address, [h.postal_code, h.city].filter(Boolean).join(" "), h.email, h.phone, h.website].filter(Boolean)
28
+ : [];
29
+ return (_jsxs("header", { className: "flex h-32 items-stretch bg-[var(--offerte-primary)] px-8 py-4 text-primary-foreground", children: [_jsxs("div", { className: "flex flex-1 items-center gap-3", children: [_jsx("div", { className: "flex h-full items-center", children: logoUrl ? _jsx("img", { src: logoUrl, alt: `${h?.company_name ?? ""} logo`, crossOrigin: "anonymous", className: "max-h-full max-w-56 object-contain" }) : _jsx("span", { className: "text-xs opacity-75", children: "BEDRIJFSLOGO" }) }), h?.show_company_name !== false ? _jsx("div", { className: "text-2xl font-semibold", children: h?.company_name || "Bedrijfsnaam" }) : null] }), _jsx("div", { className: "flex flex-col items-start justify-center text-left text-[11px] leading-4", children: contact.map((v) => _jsx("div", { children: v }, v)) })] }));
30
+ }
31
+ function Voet({ h }) {
32
+ return (_jsxs("footer", { className: "grid min-h-14 grid-cols-3 items-center gap-4 bg-[var(--offerte-secondary)] px-8 py-4 text-[10px] text-primary-foreground", children: [_jsxs("span", { children: ["KvK ", h?.chamber_of_commerce_number || "—"] }), _jsxs("span", { className: "text-center", children: ["Btw ", h?.vat_identification_number || "—"] }), _jsxs("span", { className: "text-right", children: ["IBAN ", h?.iban || "—"] })] }));
33
+ }
34
+ function Pagina({ children, h, logoUrl }) {
35
+ return (_jsxs("article", { "data-offerte-pagina": true, className: "flex min-h-[1123px] w-[794px] shrink-0 flex-col bg-card text-card-foreground shadow-md ring-1 ring-border", style: { "--offerte-primary": h?.primary_color ?? "#17365D", "--offerte-secondary": h?.secondary_color ?? "#6B7280" }, children: [_jsx(Briefhoofd, { h: h, logoUrl: logoUrl }), _jsx("div", { className: "flex-1 px-10 py-8 text-[13px] leading-[1.4]", children: children }), _jsx(Voet, { h: h })] }));
36
+ }
37
+ export const OfferteA4 = forwardRef(function OfferteA4({ waarden, offerteNummer, werkbonNummer, editable = false, onChange }, ref) {
38
+ const { huisstijl, ingesteld, logoUrl, loading, fout } = useHuisstijl();
39
+ const regels = Array.isArray(waarden.regels) ? waarden.regels : [];
40
+ const subtotaal = regels.reduce((s, r) => s + Math.round((r.quantity || 0) * (r.unit_price_cents || 0)), 0);
41
+ const btw = Math.round((subtotaal * (waarden.vatRate || 0)) / 100);
42
+ const totaal = subtotaal + btw;
43
+ const zet = (k, v) => onChange?.({ ...waarden, [k]: v });
44
+ const zetRegel = (i, patch) => zet("regels", regels.map((r, j) => (j === i ? { ...r, ...patch } : r)));
45
+ const T = ({ k, placeholder, className }) => editable ? (_jsx(AutoTekst, { value: waarden[k], placeholder: placeholder, onChange: (v) => zet(k, v), className: className })) : (_jsx("p", { className: `whitespace-pre-wrap ${className ?? ""}`, children: waarden[k] || (placeholder ? "" : "") }));
46
+ return (_jsxs("div", { ref: ref, className: "flex flex-col items-center gap-6", children: [!loading && (fout || !ingesteld) && (_jsx("p", { className: "w-[794px] rounded-md bg-warning/15 px-3 py-2 text-sm text-warning-foreground", children: fout ?? "Vul eerst de huisstijl in bij Instellingen (bedrijfsnaam, logo, KvK, btw, IBAN en kleuren)." })), _jsxs(Pagina, { h: huisstijl, logoUrl: logoUrl, children: [_jsx("h1", { className: "mb-5 text-2xl font-bold", children: "OFFERTE" }), _jsxs("div", { className: "mb-5 flex items-start justify-between gap-8", children: [_jsxs("div", { className: "w-72 space-y-1", children: [_jsx(T, { k: "klantNaam", placeholder: "Klantnaam", className: "font-semibold" }), _jsx(T, { k: "werkadres", placeholder: "Werkadres" })] }), _jsxs("dl", { className: "grid min-w-64 grid-cols-[auto_1fr] items-center gap-x-5 gap-y-1", children: [_jsx("dt", { children: "Offertenummer" }), _jsx("dd", { className: "font-medium", children: offerteNummer ?? "Concept" }), _jsx("dt", { children: "Datum offerte" }), _jsx("dd", { children: datum(new Date().toISOString()) }), _jsx("dt", { children: "Geldig t/m" }), _jsx("dd", { children: editable ? _jsx("input", { type: "date", className: invoer, value: waarden.geldigTot, onChange: (e) => zet("geldigTot", e.target.value) }) : datum(waarden.geldigTot) }), _jsx("dt", { children: "Werkbonnummer" }), _jsx("dd", { children: werkbonNummer ?? "—" })] })] }), _jsxs("div", { className: "mb-6 flex items-start gap-2", children: [_jsx("strong", { className: "shrink-0 pt-0.5", children: "Betreft:" }), _jsx("div", { className: "flex-1", children: _jsx(T, { k: "onderwerp", placeholder: "Onderwerp" }) })] }), _jsxs("section", { className: "mb-6", children: [_jsx("h2", { className: "mb-2 text-base font-bold", children: "Aanleiding en context" }), _jsx(T, { k: "aanleiding", placeholder: "Aanleiding (melding monteur)", className: "mb-2" }), _jsx(T, { k: "omschrijving", placeholder: "Toelichting op de offerte" })] }), _jsxs("section", { className: "mb-6", children: [_jsx("h2", { className: "mb-2 text-base font-bold", children: "Werkzaamheden" }), regels.length ? (_jsx("ol", { className: "list-decimal space-y-1 pl-7", children: regels.map((r, i) => _jsx("li", { className: "pl-1 whitespace-pre-wrap", children: r.description || "—" }, i)) })) : _jsx("p", { children: "Er zijn nog geen werkzaamheden toegevoegd." })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-2 text-base font-bold", children: "Risico's en aandachtspunten" }), _jsx(T, { k: "aandachtspunten", placeholder: "Aandachtspunten" })] })] }), _jsxs(Pagina, { h: huisstijl, logoUrl: logoUrl, children: [_jsxs("section", { className: "mb-7", children: [_jsx("h2", { className: "mb-4 text-base font-bold", children: "Financieel overzicht" }), _jsxs("table", { className: "w-full table-fixed border-collapse text-left", children: [_jsx("thead", { className: "border-y border-border", children: _jsxs("tr", { className: "font-bold", children: [_jsx("th", { className: "w-[44%] py-2", children: "Omschrijving" }), _jsx("th", { className: "w-[11%] py-2", children: "Eenheid" }), _jsx("th", { className: "w-[10%] py-2 text-right", children: "Aantal" }), _jsx("th", { className: "w-[15%] py-2 text-right", children: "Stukprijs" }), _jsx("th", { className: "w-[15%] py-2 text-right", children: "Totaalprijs" }), editable && _jsx("th", { className: "w-[5%]" })] }) }), _jsxs("tbody", { children: [regels.map((r, i) => (_jsxs("tr", { className: "align-top", children: [_jsx("td", { className: "py-1 pr-2", children: editable ? _jsx(AutoTekst, { value: r.description, placeholder: "Omschrijving", onChange: (v) => zetRegel(i, { description: v }) }) : _jsx("span", { className: "whitespace-pre-wrap break-words", children: r.description || "—" }) }), _jsx("td", { className: "py-1 pr-2", children: editable ? _jsx("input", { className: invoer, value: r.unit, onChange: (e) => zetRegel(i, { unit: e.target.value }) }) : r.unit }), _jsx("td", { className: "py-1 text-right", children: editable ? _jsx("input", { type: "number", className: `${invoer} text-right`, value: r.quantity, onChange: (e) => zetRegel(i, { quantity: Number(e.target.value) }) }) : r.quantity }), _jsx("td", { className: "py-1 text-right", children: editable ? _jsx("input", { type: "number", step: "0.01", className: `${invoer} text-right`, value: (r.unit_price_cents / 100).toString(), onChange: (e) => zetRegel(i, { unit_price_cents: Math.round(Number(e.target.value) * 100) }) }) : formatEuro(r.unit_price_cents) }), _jsx("td", { className: "py-1 text-right", children: formatEuro(Math.round(r.quantity * r.unit_price_cents)) }), editable && (_jsx("td", { className: "py-1 text-right", children: _jsx("button", { type: "button", "aria-label": "Regel verwijderen", className: "text-muted-foreground hover:text-destructive", onClick: () => zet("regels", regels.filter((_, j) => j !== i)), children: _jsx(Trash2, { className: "h-4 w-4" }) }) }))] }, i))), editable && (_jsx("tr", { children: _jsx("td", { colSpan: 6, className: "py-2", children: _jsxs("button", { type: "button", className: "flex items-center gap-1 text-primary", onClick: () => zet("regels", [...regels, { description: "", unit: "st", quantity: 1, unit_price_cents: 0 }]), children: [_jsx(Plus, { className: "h-4 w-4" }), " Regel toevoegen"] }) }) })), _jsxs("tr", { className: "border-t border-border", children: [_jsx("td", { colSpan: 4, className: "pt-2", children: "Subtotaal" }), _jsx("td", { className: "pt-2 text-right", children: formatEuro(subtotaal) })] }), _jsxs("tr", { children: [_jsx("td", { colSpan: 4, children: _jsxs("span", { className: "flex items-center gap-1", children: ["Btw ", editable ? _jsx("input", { type: "number", className: `${invoer} w-14 text-right`, value: waarden.vatRate, onChange: (e) => zet("vatRate", Number(e.target.value)) }) : waarden.vatRate, "%"] }) }), _jsx("td", { className: "text-right", children: formatEuro(btw) })] }), _jsxs("tr", { className: "font-bold", children: [_jsx("td", { colSpan: 4, children: "Totaalprijs incl. btw" }), _jsx("td", { className: "text-right", children: formatEuro(totaal) })] })] })] })] }), _jsxs("section", { className: "mb-8", children: [_jsx("h2", { className: "mb-3 text-base font-bold", children: "Voorwaarden en toelichting" }), _jsx(T, { k: "voorwaarden", placeholder: "Voorwaarden" })] }), _jsxs("section", { children: [_jsx("p", { className: "mb-6", children: "Voor akkoord:" }), _jsxs("div", { className: "space-y-5", children: [_jsx("p", { children: "Plaats: ................................................................................" }), _jsx("p", { children: "Datum: ................................................................................." }), _jsx("p", { children: "Naam ondertekenaar: ............................................................." }), _jsx("p", { className: "pt-6", children: "Handtekening: ......................................................................." })] })] })] })] }));
47
+ });
@@ -0,0 +1,4 @@
1
+ export declare function OfferteAkkoordPage({ token }: {
2
+ token: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=OfferteAkkoordPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteAkkoordPage.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteAkkoordPage.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,+BA4J9D"}
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Publieke akkoordpagina: klant opent de link uit de mail, bekijkt en accepteert de offerte.
3
+ // Lezen gaat uitsluitend via de unieke token; accepteren via een gevalideerde serverfunctie.
4
+ import { useEffect, useState } from "react";
5
+ import { useServerFn } from "@tanstack/react-start";
6
+ import { Button, Input, Label, supabase } from "@flowselections/core";
7
+ import { CheckCircle2, Loader2 } from "lucide-react";
8
+ import { formatEuro } from "../../lib/offertes";
9
+ import { accepteerPubliekeOfferte } from "../../lib/offerteAcceptatie.functions";
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const db = supabase;
12
+ export function OfferteAkkoordPage({ token }) {
13
+ const [offerte, setOfferte] = useState(null);
14
+ const [laden, setLaden] = useState(true);
15
+ const [naam, setNaam] = useState("");
16
+ const [email, setEmail] = useState("");
17
+ const [akkoord, setAkkoord] = useState(false);
18
+ const [bezig, setBezig] = useState(false);
19
+ const [fout, setFout] = useState(null);
20
+ const [klaar, setKlaar] = useState(false);
21
+ const accepteer = useServerFn(accepteerPubliekeOfferte);
22
+ useEffect(() => {
23
+ let actief = true;
24
+ db.rpc("quote_public_get", { _token: token }).then(({ data, error }) => {
25
+ if (!actief)
26
+ return;
27
+ setOfferte(error ? null : data);
28
+ if (!error && data?.klant_email)
29
+ setEmail(data.klant_email);
30
+ setLaden(false);
31
+ });
32
+ return () => {
33
+ actief = false;
34
+ };
35
+ }, [token]);
36
+ const accepteren = async () => {
37
+ setFout(null);
38
+ if (naam.trim().length < 2)
39
+ return setFout("Vul uw naam in.");
40
+ if (!/^\S+@\S+\.\S+$/.test(email.trim()))
41
+ return setFout("Vul een geldig e-mailadres in.");
42
+ if (!akkoord)
43
+ return setFout("Vink aan dat u akkoord gaat met de offerte en voorwaarden.");
44
+ setBezig(true);
45
+ try {
46
+ await accepteer({ data: { token, naam: naam.trim(), email: email.trim(), akkoord: true } });
47
+ setKlaar(true);
48
+ }
49
+ catch (error) {
50
+ setFout(error instanceof Error ? error.message : "Offerte accepteren is mislukt.");
51
+ }
52
+ finally {
53
+ setBezig(false);
54
+ }
55
+ };
56
+ if (laden)
57
+ return _jsx("main", { className: "flex min-h-screen items-center justify-center text-muted-foreground", children: "Laden\u2026" });
58
+ if (!offerte)
59
+ return (_jsx("main", { className: "flex min-h-screen items-center justify-center p-6 text-center text-muted-foreground", children: "Deze offerte is niet gevonden of de link is niet meer geldig." }));
60
+ const h = offerte.huisstijl ?? {};
61
+ const regels = Array.isArray(offerte.regels) ? offerte.regels : [];
62
+ const geaccepteerd = klaar || Boolean(offerte.acceptatie || offerte.signed_at);
63
+ const kanAccepteren = !geaccepteerd && ["verzonden", "bekeken"].includes(offerte.status);
64
+ const akkoordverklaring = `Ik heb de offerte en voorwaarden gelezen en ga akkoord met offerte #${offerte.offerte_number ?? ""} voor ${formatEuro(offerte.total_cents)} incl. btw.`;
65
+ return (_jsx("main", { className: "min-h-screen bg-muted px-4 py-8", children: _jsxs("div", { className: "mx-auto max-w-3xl space-y-6 rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm sm:p-10", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4 border-b border-border pb-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-lg font-semibold", children: h.company_name ?? "" }), _jsx("p", { className: "text-xs text-muted-foreground", children: [h.address, [h.postal_code, h.city].filter(Boolean).join(" "), h.email, h.phone].filter(Boolean).join(" · ") })] }), _jsxs("div", { className: "text-right", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Offerte" }), _jsx("p", { className: "font-mono text-lg font-semibold", children: offerte.offerte_number ?? "" })] })] }), _jsxs("section", { className: "space-y-2", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Voor ", offerte.klant_naam ?? "u"] }), offerte.onderwerp && _jsx("h1", { className: "text-2xl font-semibold", children: offerte.onderwerp }), offerte.omschrijving && _jsx("p", { className: "whitespace-pre-line text-sm", children: offerte.omschrijving }), offerte.aandachtspunten && (_jsx("p", { className: "whitespace-pre-line text-sm text-muted-foreground", children: offerte.aandachtspunten }))] }), _jsxs("section", { className: "overflow-x-auto", children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "border-b border-border text-left text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-2", children: "Omschrijving" }), _jsx("th", { className: "py-2 text-right", children: "Aantal" }), _jsx("th", { className: "py-2 text-right", children: "Prijs" }), _jsx("th", { className: "py-2 text-right", children: "Totaal" })] }) }), _jsx("tbody", { children: regels.map((r, i) => (_jsxs("tr", { className: "border-b border-border/60", children: [_jsx("td", { className: "py-2 pr-2", children: r.omschrijving }), _jsxs("td", { className: "py-2 text-right", children: [r.quantity, " ", r.unit ?? ""] }), _jsx("td", { className: "py-2 text-right", children: formatEuro(r.unit_price_cents) }), _jsx("td", { className: "py-2 text-right", children: formatEuro(r.line_total_cents) })] }, i))) })] }), _jsxs("div", { className: "ml-auto mt-3 w-full max-w-xs space-y-1 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Subtotaal" }), _jsx("span", { children: formatEuro(offerte.subtotal_cents) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { children: ["Btw ", Number(offerte.vat_rate ?? 21), "%"] }), _jsx("span", { children: formatEuro(offerte.vat_cents) })] }), _jsxs("div", { className: "flex justify-between border-t border-border pt-1 font-semibold", children: [_jsx("span", { children: "Totaal" }), _jsx("span", { children: formatEuro(offerte.total_cents) })] })] }), offerte.valid_until && (_jsxs("p", { className: "mt-2 text-xs text-muted-foreground", children: ["Geldig tot ", new Date(offerte.valid_until).toLocaleDateString("nl-NL")] }))] }), offerte.voorwaarden && (_jsx("section", { className: "whitespace-pre-line text-xs text-muted-foreground", children: offerte.voorwaarden })), geaccepteerd ? (_jsxs("section", { className: "flex items-start gap-3 rounded-md border border-primary/40 bg-primary/10 p-4", children: [_jsx(CheckCircle2, { className: "mt-0.5 h-5 w-5 text-primary" }), _jsxs("div", { children: [_jsx("p", { className: "font-semibold", children: "Bedankt voor uw akkoord!" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["De offerte is online geaccepteerd", offerte.acceptatie?.signer_name || offerte.signer_name || naam ? ` door ${offerte.acceptatie?.signer_name ?? offerte.signer_name ?? naam}` : "", offerte.acceptatie?.accepted_at ? ` op ${new Date(offerte.acceptatie.accepted_at).toLocaleString("nl-NL")}` : "", ". De werkzaamheden worden ingepland."] })] })] })) : kanAccepteren ? (_jsxs("section", { className: "space-y-4 border-t border-border pt-6", children: [_jsx("h2", { className: "text-lg font-semibold", children: "Akkoord geven" }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "akkoord-naam", children: "Uw naam" }), _jsx(Input, { id: "akkoord-naam", value: naam, onChange: (e) => setNaam(e.target.value), maxLength: 200 })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "akkoord-email", children: "Uw e-mailadres" }), _jsx(Input, { id: "akkoord-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), maxLength: 254 })] }), _jsxs("label", { className: "flex items-start gap-3 rounded-md border border-border bg-muted/40 p-4 text-sm", children: [_jsx("input", { type: "checkbox", checked: akkoord, onChange: (e) => setAkkoord(e.target.checked), className: "mt-1" }), _jsx("span", { children: akkoordverklaring })] }), fout && _jsx("p", { className: "text-sm text-destructive", children: fout }), _jsxs(Button, { size: "lg", className: "w-full", onClick: () => void accepteren(), disabled: bezig, children: [bezig ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(CheckCircle2, { className: "mr-2 h-4 w-4" }), "Offerte accepteren"] })] })) : (_jsx("p", { className: "border-t border-border pt-4 text-sm text-muted-foreground", children: "Deze offerte kan niet meer online worden geaccepteerd. Neem contact met ons op." }))] }) }));
66
+ }
@@ -1 +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"}
1
+ {"version":3,"file":"OfferteDetailPage.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteDetailPage.tsx"],"names":[],"mappings":"AAyCA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,+BA0gBrD"}
@@ -3,18 +3,129 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  // src/components/offertes/OfferteDetailPage.tsx — detailpagina van één offerte
4
4
  // Geen eigen <Header /> — die levert de shell via staticData.title
5
5
  // ============================================================================
6
+ import { useEffect, useMemo, useRef, useState } from "react";
6
7
  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";
8
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Textarea, supabase, toast, } from "@flowselections/core";
9
+ import { ArrowLeft, CheckCircle2, ChevronLeft, ChevronRight, Loader2, Save, Send, ShieldCheck, Wrench, XCircle, } from "lucide-react";
9
10
  import { useOfferte } from "../../hooks/useOfferte";
10
- import { useWerkbonPakketten } from "../../hooks/useWerkbonPakketten";
11
- import { OfferteRegelsEditor } from "./OfferteRegelsEditor";
12
11
  import { OfferteStatusBadge } from "./OfferteStatusBadge";
13
- import { effectieveStatus, formatDatum, formatEuro } from "../../lib/offertes";
12
+ import { OfferteA4 } from "./OfferteA4";
13
+ import { MoederWerkbonPaneel } from "./MoederWerkbonPaneel";
14
+ import { effectieveStatus } from "../../lib/offertes";
15
+ import { a4NaarPdfBase64, verstuurOfferteEmail } from "../../lib/offerteMail";
16
+ import { useHuisstijl } from "../../hooks/useHuisstijl";
17
+ import { WerkbonAanmakenPaneel } from "./WerkbonAanmakenPaneel";
14
18
  export function OfferteDetailPage({ offerteId }) {
15
19
  const navigate = useNavigate();
16
- const { offerte, klantNaam, werkbonNummer, regels, events, restantMandaat, loading, updateOfferte, addRegel, updateRegel, deleteRegel, naarWerkbon, } = useOfferte(offerteId);
17
- const { pakketten } = useWerkbonPakketten();
20
+ const { offerte, klantNaam, klantEmail, werkbonNummer, bronWerkbon, regels, restantMandaat, werkbonAanvraag, loading, reload, updateOfferte, saveDocument, maakWerkbon, } = useOfferte(offerteId);
21
+ const [waarden, setWaarden] = useState(null);
22
+ const [opslaan, setOpslaan] = useState(false);
23
+ const [verzendOpen, setVerzendOpen] = useState(false);
24
+ const [verzenden, setVerzenden] = useState(false);
25
+ const [mailAan, setMailAan] = useState("");
26
+ const [mailOnderwerp, setMailOnderwerp] = useState("");
27
+ const [mailBericht, setMailBericht] = useState("");
28
+ const [nummer, setNummer] = useState(null);
29
+ const [werkbonOpen, setWerkbonOpen] = useState(false);
30
+ const [moederOpen, setMoederOpen] = useState(false);
31
+ const [werkbonOpslaan, setWerkbonOpslaan] = useState(false);
32
+ const pdfRef = useRef(null);
33
+ const { huisstijl } = useHuisstijl();
34
+ const [lijstIds, setLijstIds] = useState([]);
35
+ useEffect(() => {
36
+ let actief = true;
37
+ supabase
38
+ .from("itk_offertes")
39
+ .select("id, status, valid_until, source_type, notities")
40
+ .order("created_at", { ascending: false })
41
+ .then((r) => {
42
+ if (!actief)
43
+ return;
44
+ const rijen = Array.isArray(r?.data) ? r.data : [];
45
+ // Zelfde filter als de overzichtslijst: geen omgezette offertes en geen
46
+ // onverzonden extra-werk-concepten (die staan alleen in het meldingenblok)
47
+ const zichtbaar = rijen.filter((o) => {
48
+ if (effectieveStatus(o) === "omgezet")
49
+ return false;
50
+ if (o.source_type === "work_order" &&
51
+ o.status === "concept" &&
52
+ typeof o.notities === "string" &&
53
+ o.notities.startsWith("Extra werk gemeld"))
54
+ return false;
55
+ return true;
56
+ });
57
+ setLijstIds(zichtbaar.map((x) => String(x.id)));
58
+ });
59
+ return () => { actief = false; };
60
+ }, []);
61
+ const lijstIndex = lijstIds.indexOf(offerteId);
62
+ const gaNaar = (i) => {
63
+ const id = lijstIds[i];
64
+ if (id)
65
+ navigate({ to: "/offertes/$offerteId", params: { offerteId: id } });
66
+ };
67
+ const documentWaarden = useMemo(() => {
68
+ if (!offerte)
69
+ return null;
70
+ const bron = bronWerkbon ?? {};
71
+ const veilig = Array.isArray(regels) ? regels : [];
72
+ return {
73
+ klantNaam: klantNaam ?? "",
74
+ werkadres: [bron.work_address, bron.room_number]
75
+ .filter((waarde) => typeof waarde === "string" && waarde.length > 0)
76
+ .join(" · "),
77
+ onderwerp: offerte.onderwerp ??
78
+ (typeof bron.technician_task_description === "string" ? bron.technician_task_description : ""),
79
+ aanleiding: typeof bron.complaint_description === "string" ? bron.complaint_description : "",
80
+ omschrijving: offerte.omschrijving ?? "",
81
+ aandachtspunten: offerte.aandachtspunten ??
82
+ (typeof bron.technician_notes === "string" ? bron.technician_notes : ""),
83
+ voorwaarden: offerte.voorwaarden ?? "",
84
+ geldigTot: offerte.valid_until?.slice(0, 10) ?? "",
85
+ vatRate: Number(offerte.vat_rate ?? 21),
86
+ regels: veilig.map((regel) => ({
87
+ description: regel.omschrijving ?? "",
88
+ unit: regel.unit ?? "st",
89
+ quantity: Number(regel.quantity ?? 0),
90
+ unit_price_cents: Number(regel.unit_price_cents ?? 0),
91
+ })),
92
+ };
93
+ }, [bronWerkbon, klantNaam, offerte, regels]);
94
+ const werkbonConcept = useMemo(() => {
95
+ if (!offerte)
96
+ return null;
97
+ const bron = bronWerkbon ?? {};
98
+ const veiligeRegels = Array.isArray(regels) ? regels : [];
99
+ return {
100
+ customer_id: offerte.customer_id,
101
+ customer_name: klantNaam ?? "",
102
+ parent_work_order_id: typeof bron.id === "string" ? bron.id : offerte.source_ref ?? offerte.werkbon_id,
103
+ parent_work_order_number: typeof bron.project_number === "string" ? bron.project_number : werkbonNummer ?? "",
104
+ work_address: typeof bron.work_address === "string" ? bron.work_address : "",
105
+ room_number: typeof bron.room_number === "string" ? bron.room_number : "",
106
+ reference: offerte.offerte_number ?? "",
107
+ complaint_description: offerte.onderwerp ??
108
+ (typeof bron.complaint_description === "string" ? bron.complaint_description : offerte.omschrijving),
109
+ technician_task_description: offerte.omschrijving ?? "",
110
+ budget_mandate_cents: Math.max(0, Number(offerte.total_cents ?? 0)),
111
+ is_follow_up: true,
112
+ estimated_duration_minutes: null,
113
+ deadline_date: null,
114
+ regels: veiligeRegels.map((regel) => ({
115
+ package_id: regel.package_id,
116
+ description: regel.omschrijving ?? "",
117
+ unit: regel.unit ?? "st",
118
+ quantity: Number(regel.quantity ?? 0),
119
+ unit_price_cents: Number(regel.unit_price_cents ?? 0),
120
+ })),
121
+ };
122
+ }, [bronWerkbon, klantNaam, offerte, regels, werkbonNummer]);
123
+ useEffect(() => {
124
+ setWaarden(documentWaarden);
125
+ }, [documentWaarden]);
126
+ useEffect(() => {
127
+ setNummer(offerte?.offerte_number ?? null);
128
+ }, [offerte?.offerte_number]);
18
129
  if (loading) {
19
130
  return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Laden\u2026" });
20
131
  }
@@ -25,24 +136,137 @@ export function OfferteDetailPage({ offerteId }) {
25
136
  const bewerkbaar = ["concept", "mandaat"].includes(status);
26
137
  const binnenMandaat = offerte.total_cents > 0 && offerte.total_cents <= restantMandaat;
27
138
  const isOmgezet = Boolean(offerte.follow_up_werkbon_id);
139
+ const wachtOpWerkbon = !isOmgezet && werkbonAanvraag?.status === "open";
140
+ const aanvraagMislukt = !isOmgezet && werkbonAanvraag?.status === "mislukt";
28
141
  const markeerMandaat = async () => {
29
142
  await updateOfferte({ status: "mandaat", binnen_mandaat: true, mandaat_restant_cents: restantMandaat }, "mandaat_bevestigd", "Binnen mandaat — geen verzending naar klant nodig");
30
143
  };
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");
144
+ const goedkeuren = async () => {
145
+ const ok = await updateOfferte({ status: "goedgekeurd", approved_at: new Date().toISOString(), rejected_at: null }, "goedgekeurd", status === "afgewezen" ? "Eerdere afwijzing gewijzigd naar akkoord" : "Offerte goedgekeurd");
146
+ if (!ok || !offerte.werkbon_concept || offerte.follow_up_werkbon_id)
147
+ return;
148
+ const { data, error } = await supabase.rpc("quote_release_work_order", { _offerte_id: offerteId });
149
+ if (error) {
150
+ toast.error(`Werkbon doorzetten mislukt: ${error.message}`);
151
+ }
152
+ else {
153
+ toast.success(`Werkbon ${data?.werkbon_nummer ?? ""} staat nu in Werkbonnen`);
154
+ }
155
+ await reload();
38
156
  };
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 })] }));
157
+ const afwijzen = () => updateOfferte({ status: "afgewezen", rejected_at: new Date().toISOString(), approved_at: null }, "afgewezen", status === "goedgekeurd" ? "Eerder akkoord gewijzigd naar afgewezen" : "Offerte afgewezen");
158
+ const werkbonAanmaken = async (concept) => {
159
+ setWerkbonOpslaan(true);
160
+ try {
161
+ const resultaat = await maakWerkbon(concept);
162
+ toast.success(resultaat.changed
163
+ ? `Werkbon ${resultaat.werkbon_nummer ?? ""} is direct aangemaakt`
164
+ : `Werkbon ${resultaat.werkbon_nummer ?? ""} bestond al`);
165
+ setWerkbonOpen(false);
166
+ }
167
+ catch (error) {
168
+ console.error(error);
169
+ toast.error(error instanceof Error ? error.message : "Werkbon aanmaken mislukt");
170
+ }
171
+ finally {
172
+ setWerkbonOpslaan(false);
173
+ }
174
+ };
175
+ const bewaarA4 = async () => {
176
+ if (!waarden)
177
+ return;
178
+ setOpslaan(true);
179
+ try {
180
+ const veilig = Array.isArray(waarden.regels) ? waarden.regels : [];
181
+ const gelukt = await saveDocument({
182
+ onderwerp: waarden.onderwerp,
183
+ omschrijving: waarden.omschrijving,
184
+ aandachtspunten: waarden.aandachtspunten,
185
+ voorwaarden: waarden.voorwaarden,
186
+ valid_until: waarden.geldigTot || null,
187
+ vat_rate: waarden.vatRate,
188
+ }, veilig);
189
+ if (gelukt)
190
+ toast.success("Offerte opgeslagen");
191
+ }
192
+ finally {
193
+ setOpslaan(false);
194
+ }
195
+ };
196
+ const openVerzenden = () => {
197
+ if (!waarden)
198
+ return;
199
+ setMailAan(klantEmail);
200
+ setMailOnderwerp(`Offerte${nummer ? ` ${nummer}` : ""}${waarden.onderwerp ? ` – ${waarden.onderwerp}` : ""}`);
201
+ const klus = waarden.onderwerp.trim() || werkbonNummer || "de werkzaamheden";
202
+ setMailBericht(`Beste ${waarden.klantNaam || "klant"},\n\n` +
203
+ `Voor ${klus} sturen wij u onderstaande offerte. In de offerte leest u precies welke werkzaamheden moeten worden uitgevoerd en wat deze kosten. De offerte vindt u als bijlage bij deze e-mail.\n\n` +
204
+ `Wij horen graag of u akkoord gaat met deze offerte.\n\n\n\n` +
205
+ `Met vriendelijke groet,\n\n${huisstijl?.company_name ?? ""}`);
206
+ setVerzendOpen(true);
207
+ };
208
+ const verstuur = async () => {
209
+ if (!waarden)
210
+ return;
211
+ const aan = mailAan.trim();
212
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(aan)) {
213
+ toast.error("Vul een geldig e-mailadres in");
214
+ return;
215
+ }
216
+ setVerzenden(true);
217
+ try {
218
+ const veilig = Array.isArray(waarden.regels) ? waarden.regels : [];
219
+ const opgeslagen = await saveDocument({
220
+ onderwerp: waarden.onderwerp,
221
+ omschrijving: waarden.omschrijving,
222
+ aandachtspunten: waarden.aandachtspunten,
223
+ voorwaarden: waarden.voorwaarden,
224
+ valid_until: waarden.geldigTot || null,
225
+ vat_rate: waarden.vatRate,
226
+ }, veilig);
227
+ if (!opgeslagen)
228
+ throw new Error("Offerte opslaan mislukt");
229
+ let definitiefNummer = nummer;
230
+ if (!definitiefNummer) {
231
+ const { data, error } = await supabase.rpc("quote_assign_number", { _offerte_id: offerteId });
232
+ if (error || typeof data !== "string")
233
+ throw new Error(error?.message ?? "Offertenummer toekennen mislukt");
234
+ definitiefNummer = data;
235
+ setNummer(data);
236
+ }
237
+ await new Promise((resolve) => setTimeout(resolve, 150));
238
+ const onderwerp = /^Offerte(\s|$)/.test(mailOnderwerp) && !mailOnderwerp.includes(definitiefNummer)
239
+ ? mailOnderwerp.replace(/^Offerte/, `Offerte ${definitiefNummer}`)
240
+ : mailOnderwerp;
241
+ const paginas = Array.from(pdfRef.current?.querySelectorAll("[data-offerte-pagina]") ?? []);
242
+ const pdfBase64 = await a4NaarPdfBase64(paginas);
243
+ await verstuurOfferteEmail({
244
+ offerteId,
245
+ to: aan,
246
+ subject: onderwerp,
247
+ message: mailBericht,
248
+ pdfBase64,
249
+ filename: `Offerte-${definitiefNummer}.pdf`,
250
+ });
251
+ await reload();
252
+ toast.success(`Offerte verzonden naar ${aan}`);
253
+ setVerzendOpen(false);
254
+ }
255
+ catch (error) {
256
+ console.error(error);
257
+ toast.error(error instanceof Error ? error.message : "Verzenden mislukt");
258
+ }
259
+ finally {
260
+ setVerzenden(false);
261
+ }
262
+ };
263
+ 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: nummer ?? "Concept" }), _jsx(OfferteStatusBadge, { status: status })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: klantNaam ?? "Onbekende klant" }), _jsx("div", { className: "mt-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: werkbonNummer ? (_jsx("p", { children: _jsxs("span", { className: "font-medium", children: ["Vervolg op werkbon ", werkbonNummer] }) })) : (_jsx("p", { className: "font-medium", children: "Nieuwe klus, geen eerdere werkbon" })) })] }), _jsxs("div", { className: "flex flex-col items-end gap-2", children: [_jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [bewerkbaar && !binnenMandaat && (_jsxs(Button, { variant: "outline", onClick: openVerzenden, 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: openVerzenden, disabled: verzenden, children: [_jsx(Send, { className: "mr-1 h-4 w-4" }), " Opnieuw verzenden"] }), _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"] })] })), status === "goedgekeurd" && !isOmgezet && !wachtOpWerkbon && (_jsxs(Button, { variant: "outline", onClick: afwijzen, children: [_jsx(XCircle, { className: "mr-1 h-4 w-4" }), " Toch afwijzen"] })), status === "afgewezen" && (_jsxs(Button, { onClick: goedkeuren, children: [_jsx(CheckCircle2, { className: "mr-1 h-4 w-4" }), " Toch akkoord"] })), !isOmgezet &&
264
+ (status === "goedgekeurd" || status === "wacht_op_werkbon" || aanvraagMislukt || status === "mandaat") && (_jsxs(Button, { onClick: () => {
265
+ setWerkbonOpen(true);
266
+ setMoederOpen(false);
267
+ }, children: [_jsx(Wrench, { className: "mr-1 h-4 w-4" }), " Werkbon aanmaken"] }))] }), lijstIndex >= 0 && lijstIds.length > 1 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", disabled: lijstIndex <= 0, onClick: () => gaNaar(lijstIndex - 1), "aria-label": "Vorige offerte", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsxs("span", { className: "min-w-24 text-center text-sm text-muted-foreground", children: ["Offerte ", lijstIndex + 1, " van ", lijstIds.length] }), _jsx(Button, { variant: "outline", size: "icon", disabled: lijstIndex >= lijstIds.length - 1, onClick: () => gaNaar(lijstIndex + 1), "aria-label": "Volgende offerte", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] }))] })] }), wachtOpWerkbon && (_jsx("p", { className: "rounded-md border border-border bg-muted/40 px-4 py-3 text-sm text-muted-foreground", children: "Er staat nog een oudere aanvraag klaar. Je kunt de werkbon hierboven nu direct aanmaken." })), aanvraagMislukt && (_jsxs("p", { className: "rounded-md border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: ["Het aanmaken van de vervolgwerkbon is niet gelukt", werkbonAanvraag?.foutmelding ? `: ${werkbonAanvraag.foutmelding}` : "", ". Je kunt de aanvraag opnieuw doen."] })), isOmgezet && werkbonAanvraag?.werkbon_nummer && (_jsxs("p", { className: "rounded-md border border-border bg-muted/40 px-4 py-3 text-sm text-muted-foreground", children: ["Vervolgwerkbon ", werkbonAanvraag.werkbon_nummer, " is aangemaakt."] })), waarden && (_jsxs("div", { className: werkbonOpen
268
+ ? moederOpen
269
+ ? "grid items-start gap-4 xl:grid-cols-[40px_minmax(280px,1fr)_620px_minmax(340px,1fr)]"
270
+ : "grid items-start gap-4 xl:grid-cols-[40px_620px_minmax(340px,1fr)]"
271
+ : "", children: [werkbonOpen && werkbonConcept && (_jsxs("button", { type: "button", onClick: () => setMoederOpen((v) => !v), "aria-expanded": moederOpen, "aria-label": moederOpen ? "Moederwerkbon terugklappen" : "Moederwerkbon uitklappen", className: "sticky top-6 flex h-fit min-h-72 w-full shrink-0 flex-col items-center justify-center gap-2 rounded-md border border-border bg-muted/60 px-1 py-4 text-muted-foreground transition-colors hover:bg-muted", children: [moederOpen ? (_jsx(ChevronLeft, { className: "h-5 w-5" })) : (_jsx(ChevronRight, { className: "h-5 w-5" })), _jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide", style: { writingMode: "vertical-rl" }, children: "Moederwerkbon" }), !moederOpen && werkbonConcept.parent_work_order_number && (_jsx("span", { className: "font-mono text-[10px]", style: { writingMode: "vertical-rl" }, children: werkbonConcept.parent_work_order_number }))] })), werkbonOpen && werkbonConcept && (_jsx("div", { className: moederOpen ? "" : "hidden", children: _jsx(MoederWerkbonPaneel, { werkbonId: werkbonConcept.parent_work_order_id, klantNaam: werkbonConcept.customer_name }) })), _jsx("div", { className: "overflow-x-auto bg-muted px-4 py-6 sm:px-6", children: _jsx("div", { style: werkbonOpen ? { zoom: 0.72 } : undefined, children: _jsx(OfferteA4, { waarden: waarden, onChange: setWaarden, editable: bewerkbaar, offerteNummer: nummer, werkbonNummer: werkbonNummer }) }) }), werkbonOpen && werkbonConcept && (_jsx(WerkbonAanmakenPaneel, { initialValue: werkbonConcept, saving: werkbonOpslaan, onCancel: () => setWerkbonOpen(false), onCreate: werkbonAanmaken }))] })), bewerkbaar && waarden && (_jsxs("div", { className: "flex justify-end gap-2 border-t pt-4", children: [_jsxs(Button, { onClick: () => void bewaarA4(), disabled: opslaan, children: [opslaan ? _jsx(Loader2, { className: "mr-1 h-4 w-4 animate-spin" }) : _jsx(Save, { className: "mr-1 h-4 w-4" }), "Offerte opslaan"] }), !binnenMandaat && (_jsxs(Button, { variant: "outline", onClick: openVerzenden, disabled: opslaan || verzenden, children: [_jsx(Send, { className: "mr-1 h-4 w-4" }), " Verzenden"] }))] })), waarden && (_jsx("div", { "aria-hidden": true, className: "pointer-events-none fixed -left-[10000px] top-0", children: _jsx(OfferteA4, { ref: pdfRef, waarden: waarden, offerteNummer: nummer, werkbonNummer: werkbonNummer }) })), _jsx(Dialog, { open: verzendOpen, onOpenChange: setVerzendOpen, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Offerte verzenden" }) }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "detail-mail-aan", children: "E-mailadres" }), _jsx(Input, { id: "detail-mail-aan", type: "email", value: mailAan, onChange: (event) => setMailAan(event.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "detail-mail-onderwerp", children: "Onderwerp" }), _jsx(Input, { id: "detail-mail-onderwerp", value: mailOnderwerp, onChange: (event) => setMailOnderwerp(event.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "detail-mail-bericht", children: "Bericht" }), _jsx(Textarea, { id: "detail-mail-bericht", rows: 10, value: mailBericht, onChange: (event) => setMailBericht(event.target.value) })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "De offerte wordt eerst opgeslagen en als PDF-bijlage meegestuurd." })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setVerzendOpen(false), disabled: verzenden, children: "Annuleren" }), _jsxs(Button, { onClick: () => void verstuur(), disabled: verzenden, children: [verzenden ? _jsx(Loader2, { className: "mr-1 h-4 w-4 animate-spin" }) : _jsx(Send, { className: "mr-1 h-4 w-4" }), "Versturen"] })] })] }) })] }));
48
272
  }
@@ -21,7 +21,7 @@ export function OfferteRegelsEditor({ regels, pakketten, readOnly, onAdd, onUpda
21
21
  setPakketId("");
22
22
  };
23
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, {
24
+ onUpdate(regel.id, { omschrijving: e.target.value }) })) }), _jsx(TableCell, { className: "text-right", children: readOnly ? (Number(regel.quantity)) : (_jsx(Input, { type: "number", step: "1", 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
25
  unit_price_cents: Math.round((Number(e.target.value) || 0) * 100),
26
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
27
  }
@@ -1 +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"}
1
+ {"version":3,"file":"OfferteStatusBadge.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OfferteStatusBadge.tsx"],"names":[],"mappings":"AAeA,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,+BAShE"}
@@ -7,6 +7,7 @@ const STYLES = {
7
7
  verzonden: "bg-secondary text-secondary-foreground border-transparent",
8
8
  bekeken: "bg-secondary text-secondary-foreground border-transparent",
9
9
  goedgekeurd: "bg-primary text-primary-foreground border-transparent",
10
+ wacht_op_werkbon: "bg-accent text-accent-foreground border-transparent",
10
11
  omgezet: "bg-primary text-primary-foreground border-transparent",
11
12
  afgewezen: "bg-destructive text-destructive-foreground border-transparent",
12
13
  verlopen: "bg-background text-muted-foreground border-border",
@@ -1 +1 @@
1
- {"version":3,"file":"OffertesOverzichtPage.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OffertesOverzichtPage.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,gCAiJpC"}
1
+ {"version":3,"file":"OffertesOverzichtPage.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/OffertesOverzichtPage.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,gCAwOpC"}
@@ -8,9 +8,9 @@ import { useNavigate } from "@tanstack/react-router";
8
8
  import { Button, Card, CardContent, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsList, TabsTrigger, cn, } from "@flowselections/core";
9
9
  import { Building2, Plus, Search } from "lucide-react";
10
10
  import { useOffertes } from "../../hooks/useOffertes";
11
- import { OfferteKpiCards } from "./OfferteKpiCards";
12
11
  import { OfferteStatusBadge } from "./OfferteStatusBadge";
13
- import { NieuweOfferteDialog } from "./NieuweOfferteDialog";
12
+ import { NieuweOfferteFormulier } from "./NieuweOfferteFormulier";
13
+ import { ExtraWerkMeldingenLijst } from "./ExtraWerkMeldingenLijst";
14
14
  import { OFFERTE_TABS, effectieveStatus, formatDatum, formatEuro } from "../../lib/offertes";
15
15
  export function OffertesOverzichtPage() {
16
16
  const { offertes, loading, reload } = useOffertes();
@@ -24,6 +24,15 @@ export function OffertesOverzichtPage() {
24
24
  const term = zoek.trim().toLowerCase();
25
25
  return lijst.filter((o) => {
26
26
  const status = effectieveStatus(o);
27
+ if (status === "omgezet")
28
+ return false;
29
+ // Open extra-werkmeldingen staan alleen in het meldingenblok tot ze verzonden zijn
30
+ const anyO = o;
31
+ if (anyO.source_type === "work_order" &&
32
+ anyO.status === "concept" &&
33
+ typeof anyO.notities === "string" &&
34
+ anyO.notities.startsWith("Extra werk gemeld"))
35
+ return false;
27
36
  if (tabDef?.statuses && !tabDef.statuses.includes(status))
28
37
  return false;
29
38
  if (!term)
@@ -33,7 +42,20 @@ export function OffertesOverzichtPage() {
33
42
  .some((v) => String(v).toLowerCase().includes(term));
34
43
  });
35
44
  }, [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) => {
45
+ const omgezet = useMemo(() => {
46
+ const lijst = Array.isArray(offertes) ? offertes : [];
47
+ const term = zoek.trim().toLowerCase();
48
+ return lijst.filter((o) => {
49
+ if (effectieveStatus(o) !== "omgezet")
50
+ return false;
51
+ if (!term)
52
+ return true;
53
+ return [o.offerte_number, o.klant_naam, o.omschrijving, o.werkbon_nummer]
54
+ .filter(Boolean)
55
+ .some((v) => String(v).toLowerCase().includes(term));
56
+ });
57
+ }, [offertes, zoek]);
58
+ 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(ExtraWerkMeldingenLijst, { onOfferteGemaakt: reload }), _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) })] })] }), omgezet.length > 0 && (_jsx(Card, { children: _jsxs(CardContent, { className: "p-0", children: [_jsxs("div", { className: "border-b px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Omgezette offertes" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Deze offertes zijn omgezet naar een werkbon \u2014 alleen nog ter inzage." })] }), _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" })] }) }), _jsx(TableBody, { children: omgezet.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 ?? "Concept" }), _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(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 ?? "Concept" }), _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(NieuweOfferteFormulier, { open: dialogOpen, onOpenChange: setDialogOpen, onCreated: (id) => {
37
59
  reload();
38
60
  navigate({ to: "/offertes/$offerteId", params: { offerteId: id } });
39
61
  } }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Werkbonnen, monteurnotities en pakketten worden beheerd in de module Werkbonnen." })] }));
@@ -0,0 +1,14 @@
1
+ import { type WerkbonVoorstel } from "../../hooks/useWerkbonVoorstellen";
2
+ import { type AiOfferteConcept } from "../../lib/offerteAi.functions";
3
+ interface Props {
4
+ voorstel: WerkbonVoorstel;
5
+ index: number;
6
+ aantalVoorstellen: number;
7
+ onNavigate: (index: number) => void;
8
+ onClose: () => void;
9
+ onCreated: (offerteId: string) => void | Promise<void>;
10
+ onSaveConcept: (c: AiOfferteConcept) => void;
11
+ }
12
+ export declare function VoorstelOffertePaneel({ voorstel, index, aantalVoorstellen, onNavigate, onClose, onCreated, onSaveConcept }: Props): import("react").JSX.Element;
13
+ export {};
14
+ //# sourceMappingURL=VoorstelOffertePaneel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoorstelOffertePaneel.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/VoorstelOffertePaneel.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAW9F,UAAU,KAAK;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,aAAa,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC9C;AAkBD,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,KAAK,+BAqXjI"}