@flowselections/offertes-installatie-techniek 1.0.2 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist-lib/components/offertes/ArtikelKiezer.d.ts +16 -0
- package/dist-lib/components/offertes/ArtikelKiezer.d.ts.map +1 -0
- package/dist-lib/components/offertes/ArtikelKiezer.js +56 -0
- package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts +4 -0
- package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts.map +1 -0
- package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.js +42 -0
- package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts +14 -0
- package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/ExtraWerkOffertePaneel.js +91 -0
- package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts +5 -0
- package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/MoederWerkbonPaneel.js +66 -0
- package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts +33 -0
- package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts.map +1 -0
- package/dist-lib/components/offertes/NieuweOfferteFormulier.js +523 -0
- package/dist-lib/components/offertes/OfferteA4.d.ts +28 -0
- package/dist-lib/components/offertes/OfferteA4.d.ts.map +1 -0
- package/dist-lib/components/offertes/OfferteA4.js +47 -0
- package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts +4 -0
- package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts.map +1 -0
- package/dist-lib/components/offertes/OfferteAkkoordPage.js +66 -0
- package/dist-lib/components/offertes/OfferteDetailPage.d.ts.map +1 -1
- package/dist-lib/components/offertes/OfferteDetailPage.js +247 -23
- package/dist-lib/components/offertes/OfferteRegelsEditor.js +1 -1
- package/dist-lib/components/offertes/OfferteStatusBadge.d.ts.map +1 -1
- package/dist-lib/components/offertes/OfferteStatusBadge.js +1 -0
- package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts.map +1 -1
- package/dist-lib/components/offertes/OffertesOverzichtPage.js +25 -3
- package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts +14 -0
- package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/VoorstelOffertePaneel.js +144 -0
- package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts +32 -0
- package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts.map +1 -0
- package/dist-lib/components/offertes/WerkbonAanmakenPaneel.js +26 -0
- package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts +4 -0
- package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts.map +1 -0
- package/dist-lib/components/offertes/WerkbonVoorstellenLijst.js +47 -0
- package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/OfferteEmailSettingsCard.js +38 -0
- package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.js +66 -0
- package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.js +30 -0
- package/dist-lib/hooks/useExtraWerkMeldingen.d.ts +32 -0
- package/dist-lib/hooks/useExtraWerkMeldingen.d.ts.map +1 -0
- package/dist-lib/hooks/useExtraWerkMeldingen.js +103 -0
- package/dist-lib/hooks/useHuisstijl.d.ts +32 -0
- package/dist-lib/hooks/useHuisstijl.d.ts.map +1 -0
- package/dist-lib/hooks/useHuisstijl.js +60 -0
- package/dist-lib/hooks/useOfferte.d.ts +21 -0
- package/dist-lib/hooks/useOfferte.d.ts.map +1 -1
- package/dist-lib/hooks/useOfferte.js +105 -4
- package/dist-lib/hooks/useOffertes.d.ts.map +1 -1
- package/dist-lib/hooks/useOffertes.js +17 -7
- package/dist-lib/hooks/useWerkbonVoorstellen.d.ts +45 -0
- package/dist-lib/hooks/useWerkbonVoorstellen.d.ts.map +1 -0
- package/dist-lib/hooks/useWerkbonVoorstellen.js +74 -0
- package/dist-lib/index.d.ts +19 -2
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +26 -4
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +4 -23403
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +2 -23403
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2 -23403
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
- package/dist-lib/integrations/supabase/types.d.ts +23504 -11091
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +2 -0
- package/dist-lib/lib/artikelenZoek.d.ts +17 -0
- package/dist-lib/lib/artikelenZoek.d.ts.map +1 -0
- package/dist-lib/lib/artikelenZoek.js +37 -0
- package/dist-lib/lib/offerteAcceptatie.functions.d.ts +7 -0
- package/dist-lib/lib/offerteAcceptatie.functions.d.ts.map +1 -0
- package/dist-lib/lib/offerteAcceptatie.functions.js +28 -0
- package/dist-lib/lib/offerteAdres.d.ts +7 -0
- package/dist-lib/lib/offerteAdres.d.ts.map +1 -0
- package/dist-lib/lib/offerteAdres.js +19 -0
- package/dist-lib/lib/offerteAi.functions.d.ts +34 -0
- package/dist-lib/lib/offerteAi.functions.d.ts.map +1 -0
- package/dist-lib/lib/offerteAi.functions.js +252 -0
- package/dist-lib/lib/offerteMail.d.ts +27 -0
- package/dist-lib/lib/offerteMail.d.ts.map +1 -0
- package/dist-lib/lib/offerteMail.js +141 -0
- package/dist-lib/lib/offerteRegels.d.ts +9 -0
- package/dist-lib/lib/offerteRegels.d.ts.map +1 -0
- package/dist-lib/lib/offerteRegels.js +20 -0
- package/dist-lib/lib/offerteRegels.test.d.ts +2 -0
- package/dist-lib/lib/offerteRegels.test.d.ts.map +1 -0
- package/dist-lib/lib/offerteRegels.test.js +17 -0
- package/dist-lib/lib/offerteReistijd.functions.d.ts +22 -0
- package/dist-lib/lib/offerteReistijd.functions.d.ts.map +1 -0
- package/dist-lib/lib/offerteReistijd.functions.js +173 -0
- package/dist-lib/lib/offertes.d.ts +8 -1
- package/dist-lib/lib/offertes.d.ts.map +1 -1
- package/dist-lib/lib/offertes.js +6 -4
- package/package.json +96 -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 @@
|
|
|
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":"
|
|
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,
|
|
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 {
|
|
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,
|
|
17
|
-
const
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
toast.
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
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: "
|
|
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":"
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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"}
|