@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.
Files changed (105) hide show
  1. package/README.md +16 -1
  2. package/dist-lib/components/offertes/ArtikelKiezer.d.ts +16 -0
  3. package/dist-lib/components/offertes/ArtikelKiezer.d.ts.map +1 -0
  4. package/dist-lib/components/offertes/ArtikelKiezer.js +56 -0
  5. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts +4 -0
  6. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.d.ts.map +1 -0
  7. package/dist-lib/components/offertes/ExtraWerkMeldingenLijst.js +42 -0
  8. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts +14 -0
  9. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.d.ts.map +1 -0
  10. package/dist-lib/components/offertes/ExtraWerkOffertePaneel.js +91 -0
  11. package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts +5 -0
  12. package/dist-lib/components/offertes/MoederWerkbonPaneel.d.ts.map +1 -0
  13. package/dist-lib/components/offertes/MoederWerkbonPaneel.js +66 -0
  14. package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts +33 -0
  15. package/dist-lib/components/offertes/NieuweOfferteFormulier.d.ts.map +1 -0
  16. package/dist-lib/components/offertes/NieuweOfferteFormulier.js +523 -0
  17. package/dist-lib/components/offertes/OfferteA4.d.ts +28 -0
  18. package/dist-lib/components/offertes/OfferteA4.d.ts.map +1 -0
  19. package/dist-lib/components/offertes/OfferteA4.js +47 -0
  20. package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts +4 -0
  21. package/dist-lib/components/offertes/OfferteAkkoordPage.d.ts.map +1 -0
  22. package/dist-lib/components/offertes/OfferteAkkoordPage.js +66 -0
  23. package/dist-lib/components/offertes/OfferteDetailPage.d.ts.map +1 -1
  24. package/dist-lib/components/offertes/OfferteDetailPage.js +247 -23
  25. package/dist-lib/components/offertes/OfferteRegelsEditor.js +1 -1
  26. package/dist-lib/components/offertes/OfferteStatusBadge.d.ts.map +1 -1
  27. package/dist-lib/components/offertes/OfferteStatusBadge.js +1 -0
  28. package/dist-lib/components/offertes/OffertesOverzichtPage.d.ts.map +1 -1
  29. package/dist-lib/components/offertes/OffertesOverzichtPage.js +25 -3
  30. package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts +14 -0
  31. package/dist-lib/components/offertes/VoorstelOffertePaneel.d.ts.map +1 -0
  32. package/dist-lib/components/offertes/VoorstelOffertePaneel.js +144 -0
  33. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts +32 -0
  34. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.d.ts.map +1 -0
  35. package/dist-lib/components/offertes/WerkbonAanmakenPaneel.js +26 -0
  36. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts +4 -0
  37. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.d.ts.map +1 -0
  38. package/dist-lib/components/offertes/WerkbonVoorstellenLijst.js +47 -0
  39. package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts +2 -0
  40. package/dist-lib/components/settings/OfferteEmailSettingsCard.d.ts.map +1 -0
  41. package/dist-lib/components/settings/OfferteEmailSettingsCard.js +38 -0
  42. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts +2 -0
  43. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.d.ts.map +1 -0
  44. package/dist-lib/components/settings/OfferteHuisstijlSettingsCard.js +66 -0
  45. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts +2 -0
  46. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.d.ts.map +1 -0
  47. package/dist-lib/components/settings/OfferteVoorwaardenSettingsCard.js +30 -0
  48. package/dist-lib/hooks/useExtraWerkMeldingen.d.ts +32 -0
  49. package/dist-lib/hooks/useExtraWerkMeldingen.d.ts.map +1 -0
  50. package/dist-lib/hooks/useExtraWerkMeldingen.js +103 -0
  51. package/dist-lib/hooks/useHuisstijl.d.ts +32 -0
  52. package/dist-lib/hooks/useHuisstijl.d.ts.map +1 -0
  53. package/dist-lib/hooks/useHuisstijl.js +60 -0
  54. package/dist-lib/hooks/useOfferte.d.ts +21 -0
  55. package/dist-lib/hooks/useOfferte.d.ts.map +1 -1
  56. package/dist-lib/hooks/useOfferte.js +105 -4
  57. package/dist-lib/hooks/useOffertes.d.ts.map +1 -1
  58. package/dist-lib/hooks/useOffertes.js +17 -7
  59. package/dist-lib/hooks/useWerkbonVoorstellen.d.ts +45 -0
  60. package/dist-lib/hooks/useWerkbonVoorstellen.d.ts.map +1 -0
  61. package/dist-lib/hooks/useWerkbonVoorstellen.js +74 -0
  62. package/dist-lib/index.d.ts +19 -2
  63. package/dist-lib/index.d.ts.map +1 -1
  64. package/dist-lib/index.js +26 -4
  65. package/dist-lib/integrations/supabase/auth-middleware.d.ts +4 -23403
  66. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  67. package/dist-lib/integrations/supabase/auth-middleware.js +1 -1
  68. package/dist-lib/integrations/supabase/client.d.ts +2 -23403
  69. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  70. package/dist-lib/integrations/supabase/client.server.d.ts +2 -23403
  71. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  72. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
  73. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  74. package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
  75. package/dist-lib/integrations/supabase/types.d.ts +23504 -11091
  76. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  77. package/dist-lib/integrations/supabase/types.js +2 -0
  78. package/dist-lib/lib/artikelenZoek.d.ts +17 -0
  79. package/dist-lib/lib/artikelenZoek.d.ts.map +1 -0
  80. package/dist-lib/lib/artikelenZoek.js +37 -0
  81. package/dist-lib/lib/offerteAcceptatie.functions.d.ts +7 -0
  82. package/dist-lib/lib/offerteAcceptatie.functions.d.ts.map +1 -0
  83. package/dist-lib/lib/offerteAcceptatie.functions.js +28 -0
  84. package/dist-lib/lib/offerteAdres.d.ts +7 -0
  85. package/dist-lib/lib/offerteAdres.d.ts.map +1 -0
  86. package/dist-lib/lib/offerteAdres.js +19 -0
  87. package/dist-lib/lib/offerteAi.functions.d.ts +34 -0
  88. package/dist-lib/lib/offerteAi.functions.d.ts.map +1 -0
  89. package/dist-lib/lib/offerteAi.functions.js +252 -0
  90. package/dist-lib/lib/offerteMail.d.ts +27 -0
  91. package/dist-lib/lib/offerteMail.d.ts.map +1 -0
  92. package/dist-lib/lib/offerteMail.js +141 -0
  93. package/dist-lib/lib/offerteRegels.d.ts +9 -0
  94. package/dist-lib/lib/offerteRegels.d.ts.map +1 -0
  95. package/dist-lib/lib/offerteRegels.js +20 -0
  96. package/dist-lib/lib/offerteRegels.test.d.ts +2 -0
  97. package/dist-lib/lib/offerteRegels.test.d.ts.map +1 -0
  98. package/dist-lib/lib/offerteRegels.test.js +17 -0
  99. package/dist-lib/lib/offerteReistijd.functions.d.ts +22 -0
  100. package/dist-lib/lib/offerteReistijd.functions.d.ts.map +1 -0
  101. package/dist-lib/lib/offerteReistijd.functions.js +173 -0
  102. package/dist-lib/lib/offertes.d.ts +8 -1
  103. package/dist-lib/lib/offertes.d.ts.map +1 -1
  104. package/dist-lib/lib/offertes.js +6 -4
  105. package/package.json +96 -5
@@ -0,0 +1,144 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Groot paneel: links het werkbonvoorstel, rechts het bewerkbare AI-offertevoorstel.
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { useNavigate } from "@tanstack/react-router";
5
+ import { useServerFn } from "@tanstack/react-start";
6
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, supabase, toast, } from "@flowselections/core";
7
+ import { ChevronLeft, ChevronRight, Loader2, Plus, RefreshCw, Trash2, Wand2, X } from "lucide-react";
8
+ import { laadVoorstelRegels, } from "../../hooks/useWerkbonVoorstellen";
9
+ import { genereerOfferteConcept } from "../../lib/offerteAi.functions";
10
+ import { formatDatum, formatEuro } from "../../lib/offertes";
11
+ const db = supabase;
12
+ function plusDagen(n) {
13
+ const d = new Date();
14
+ d.setDate(d.getDate() + n);
15
+ return d.toISOString().slice(0, 10);
16
+ }
17
+ function Veld({ label, value }) {
18
+ if (value === null || value === undefined || value === "")
19
+ return null;
20
+ return (_jsxs("div", { className: "space-y-0.5", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: "whitespace-pre-wrap text-sm", children: value })] }));
21
+ }
22
+ export function VoorstelOffertePaneel({ voorstel, index, aantalVoorstellen, onNavigate, onClose, onCreated, onSaveConcept }) {
23
+ const navigate = useNavigate();
24
+ const genereer = useServerFn(genereerOfferteConcept);
25
+ const [bronRegels, setBronRegels] = useState([]);
26
+ const [regels, setRegels] = useState([]);
27
+ const [omschrijving, setOmschrijving] = useState("");
28
+ const [vat, setVat] = useState(21);
29
+ const [geldigTot, setGeldigTot] = useState(plusDagen(30));
30
+ const [aiBezig, setAiBezig] = useState(false);
31
+ const [aiFout, setAiFout] = useState(null);
32
+ const [opslaan, setOpslaan] = useState(false);
33
+ const pasToe = (c) => {
34
+ setRegels(Array.isArray(c.regels) ? c.regels : []);
35
+ setOmschrijving(c.omschrijving ?? "");
36
+ setVat(c.vat_rate ?? 21);
37
+ setGeldigTot(plusDagen(c.geldig_dagen ?? 30));
38
+ };
39
+ const draaiAi = async (lijnen) => {
40
+ setAiBezig(true);
41
+ setAiFout(null);
42
+ try {
43
+ const { data: sess } = await supabase.auth.getSession();
44
+ const token = sess?.session?.access_token;
45
+ if (!token)
46
+ throw new Error("Niet ingelogd");
47
+ const { data: pk } = await db.rpc("quote_list_packages");
48
+ const pakketten = (Array.isArray(pk) ? pk : []).map((p) => ({
49
+ id: p.id,
50
+ code: p.code,
51
+ name: p.name,
52
+ unit_price_cents: p.unit_price_cents,
53
+ }));
54
+ const { klant_naam, ai_concept, seen_at, offerte_id, ...bron } = voorstel;
55
+ const res = await genereer({
56
+ data: { accessToken: token, voorstel: { ...bron, klant_naam }, regels: lijnen, pakketten },
57
+ });
58
+ if (res.error || !res.concept) {
59
+ setAiFout(res.error ?? "AI kon geen offerte opstellen.");
60
+ return;
61
+ }
62
+ pasToe(res.concept);
63
+ onSaveConcept(res.concept);
64
+ }
65
+ catch (e) {
66
+ setAiFout(e?.message ?? "AI-verzoek mislukt");
67
+ }
68
+ finally {
69
+ setAiBezig(false);
70
+ }
71
+ };
72
+ useEffect(() => {
73
+ (async () => {
74
+ const lijnen = await laadVoorstelRegels(voorstel.id);
75
+ setBronRegels(lijnen);
76
+ const bestaand = voorstel.ai_concept;
77
+ if (bestaand && Array.isArray(bestaand.regels))
78
+ pasToe(bestaand);
79
+ else
80
+ draaiAi(lijnen);
81
+ })();
82
+ // eslint-disable-next-line react-hooks/exhaustive-deps
83
+ }, [voorstel.id]);
84
+ const subtotaal = useMemo(() => regels.reduce((s, r) => s + Math.round((r.quantity || 0) * (r.unit_price_cents || 0)), 0), [regels]);
85
+ const btw = Math.round((subtotaal * (vat || 0)) / 100);
86
+ const totaal = subtotaal + btw;
87
+ const mandaat = voorstel.budget_mandate_cents ?? 0;
88
+ const binnenMandaat = mandaat > 0 && totaal <= mandaat;
89
+ const wijzig = (i, patch) => setRegels((rs) => rs.map((r, j) => (j === i ? { ...r, ...patch } : r)));
90
+ const maakAan = async () => {
91
+ setOpslaan(true);
92
+ try {
93
+ const { data, error } = await db
94
+ .from("itk_offertes")
95
+ .insert({
96
+ source_type: "werkbon_voorstel",
97
+ source_ref: voorstel.id,
98
+ source_number: voorstel.externe_referentie || voorstel.subject || null,
99
+ customer_id: voorstel.customer_id,
100
+ omschrijving,
101
+ vat_rate: vat,
102
+ valid_until: geldigTot || null,
103
+ mandaat_restant_cents: mandaat,
104
+ binnen_mandaat: binnenMandaat,
105
+ ai_generated: true,
106
+ })
107
+ .select("id")
108
+ .single();
109
+ if (error || !data)
110
+ throw error ?? new Error("geen id");
111
+ const id = data.id;
112
+ if (regels.length) {
113
+ const { error: rErr } = await db.from("itk_offerte_regels").insert(regels.map((r, i) => ({
114
+ offerte_id: id,
115
+ source: "ai",
116
+ omschrijving: r.description,
117
+ quantity: r.quantity,
118
+ unit_price_cents: Math.round(r.unit_price_cents),
119
+ sort_order: i,
120
+ })));
121
+ if (rErr)
122
+ throw rErr;
123
+ }
124
+ await db.from("itk_offerte_events").insert({
125
+ offerte_id: id,
126
+ event_type: "aangemaakt",
127
+ omschrijving: `Aangemaakt vanuit werkbonvoorstel ${voorstel.subject ?? ""}`.trim(),
128
+ });
129
+ await onCreated(id);
130
+ toast.success("Offerte aangemaakt");
131
+ navigate({ to: "/offertes/$offerteId", params: { offerteId: id } });
132
+ }
133
+ catch (e) {
134
+ console.error(e);
135
+ toast.error("Offerte aanmaken mislukt");
136
+ }
137
+ finally {
138
+ setOpslaan(false);
139
+ }
140
+ };
141
+ return (_jsxs("div", { className: "fixed inset-y-0 right-0 z-50 flex flex-col bg-background shadow-2xl", style: { left: "var(--flow-sidebar-width, 16rem)" }, children: [_jsxs("div", { className: "flex items-center justify-between border-b px-6 py-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Offerte opstellen uit werkbonvoorstel" }), _jsx("p", { className: "text-xs text-muted-foreground", children: voorstel.klant_naam ?? voorstel.from_name ?? voorstel.from_email })] }), _jsxs("div", { className: "flex items-center gap-2", children: [aantalVoorstellen > 1 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", disabled: index <= 0, onClick: () => onNavigate(index - 1), "aria-label": "Vorig voorstel", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsxs("span", { className: "min-w-20 text-center text-sm text-muted-foreground", children: ["Voorstel ", index + 1, " van ", aantalVoorstellen] }), _jsx(Button, { variant: "outline", size: "icon", disabled: index >= aantalVoorstellen - 1, onClick: () => onNavigate(index + 1), "aria-label": "Volgend voorstel", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })), _jsx(Button, { variant: "ghost", size: "icon", onClick: onClose, "aria-label": "Sluiten", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-1 overflow-hidden lg:grid-cols-2", children: [_jsxs("div", { className: "space-y-4 overflow-y-auto border-r p-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Werkbonvoorstel" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(Veld, { label: "Onderwerp", value: voorstel.subject }), _jsx(Veld, { label: "Afzender", value: [voorstel.from_name, voorstel.from_email].filter(Boolean).join(" · ") }), _jsx(Veld, { label: "Ontvangen", value: formatDatum(voorstel.received_at) }), _jsx(Veld, { label: "Klant", value: voorstel.klant_naam }), _jsx(Veld, { label: "Werkadres", value: [voorstel.work_address, voorstel.room_number].filter(Boolean).join(" · ") }), _jsx(Veld, { label: "Klacht", value: voorstel.complaint_description }), _jsx(Veld, { label: "Taak monteur", value: voorstel.technician_task_description }), _jsx(Veld, { label: "Gepland", value: voorstel.scheduled_start_time ? formatDatum(voorstel.scheduled_start_time) : null }), _jsx(Veld, { label: "Geschatte duur", value: voorstel.estimated_duration_minutes ? `${voorstel.estimated_duration_minutes} min` : null }), _jsx(Veld, { label: "Deadline", value: voorstel.deadline_date ? formatDatum(voorstel.deadline_date) : null }), _jsx(Veld, { label: "Externe referentie", value: voorstel.externe_referentie }), _jsx(Veld, { label: "Mandaat", value: mandaat ? formatEuro(mandaat) : null }), _jsx(Veld, { label: "Interne notities", value: voorstel.internal_notes })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Regels in voorstel" }) }), _jsxs(CardContent, { className: "space-y-2", children: [bronRegels.length === 0 && _jsx("p", { className: "text-sm text-muted-foreground", children: "Geen regels." }), bronRegels.map((r) => (_jsxs("div", { className: "flex justify-between gap-2 text-sm", children: [_jsxs("span", { children: [r.description || r.article_number || r.line_type, r.note && _jsx("span", { className: "block text-xs text-muted-foreground", children: r.note })] }), _jsxs("span", { className: "text-muted-foreground", children: [r.quantity ?? "", " ", r.unit ?? ""] })] }, r.id)))] })] })] }), _jsxs("div", { className: "space-y-4 overflow-y-auto p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("h3", { className: "flex items-center gap-2 font-semibold", children: [_jsx(Wand2, { className: "h-4 w-4 text-primary" }), " Offertevoorstel"] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: aiBezig, onClick: () => draaiAi(bronRegels), children: [aiBezig ? _jsx(Loader2, { className: "mr-1 h-4 w-4 animate-spin" }) : _jsx(RefreshCw, { className: "mr-1 h-4 w-4" }), "Opnieuw laten opstellen door AI"] })] }), aiBezig && _jsx("p", { className: "text-sm text-muted-foreground", children: "AI stelt de offerte op\u2026" }), aiFout && _jsx("p", { className: "rounded-md bg-destructive/10 p-2 text-sm text-destructive", children: aiFout }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Offerteregels" }) }), _jsxs(CardContent, { className: "space-y-3", children: [regels.map((r, i) => (_jsxs("div", { className: "space-y-2 rounded-md border p-3", children: [_jsx(Textarea, { rows: 2, placeholder: "Omschrijving", value: r.description, onChange: (e) => wijzig(i, { description: e.target.value }) }), _jsxs("div", { className: "flex items-end gap-2", children: [_jsxs("div", { className: "w-24 space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: "Aantal" }), _jsx(Input, { type: "number", className: "text-right", value: r.quantity, onChange: (e) => wijzig(i, { quantity: Number(e.target.value) }) })] }), _jsxs("div", { className: "w-32 space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: "Stukprijs (\u20AC)" }), _jsx(Input, { type: "number", step: "0.01", className: "text-right", value: (r.unit_price_cents / 100).toString(), onChange: (e) => wijzig(i, { unit_price_cents: Math.round(Number(e.target.value) * 100) }) })] }), _jsxs("div", { className: "ml-auto space-y-1 text-right", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Totaal" }), _jsx("p", { className: "text-sm font-medium", children: formatEuro(Math.round(r.quantity * r.unit_price_cents)) })] }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Regel verwijderen", onClick: () => setRegels((rs) => rs.filter((_, j) => j !== i)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }, i))), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setRegels((rs) => [...rs, { description: "", quantity: 1, unit_price_cents: 0 }]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Regel toevoegen"] }), _jsxs("div", { className: "ml-auto mt-4 w-64 space-y-1 border-t pt-3 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Subtotaal" }), _jsx("span", { children: formatEuro(subtotaal) })] }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-1 text-muted-foreground", children: ["Btw", _jsx(Input, { type: "number", className: "h-7 w-16 text-right", value: vat, onChange: (e) => setVat(Number(e.target.value)) }), "%"] }), _jsx("span", { children: formatEuro(btw) })] }), _jsxs("div", { className: "flex justify-between text-base font-semibold", children: [_jsx("span", { children: "Totaal" }), _jsx("span", { children: formatEuro(totaal) })] })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Omschrijving" }) }), _jsx(CardContent, { children: _jsx(Textarea, { rows: 4, value: omschrijving, onChange: (e) => setOmschrijving(e.target.value) }) })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", 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(mandaat) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Offertetotaal" }), _jsx("span", { className: "font-medium", children: formatEuro(totaal) })] }), _jsx("p", { className: "rounded-md bg-muted p-2 text-muted-foreground", children: binnenMandaat
142
+ ? "Binnen mandaat — kan direct worden omgezet naar een werkbon."
143
+ : "Buiten mandaat — de klant moet deze offerte eerst goedkeuren." })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gegevens" }) }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "geldig", children: "Geldig t/m" }), _jsx(Input, { id: "geldig", type: "date", value: geldigTot, onChange: (e) => setGeldigTot(e.target.value) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Klant" }), _jsx("span", { children: voorstel.klant_naam ?? "—" })] })] })] })] })] })] }), _jsxs("div", { className: "flex justify-end gap-2 border-t px-6 py-3", children: [_jsx(Button, { variant: "outline", onClick: onClose, children: "Sluiten" }), _jsxs(Button, { onClick: maakAan, disabled: opslaan || aiBezig, children: [opslaan && _jsx(Loader2, { className: "mr-1 h-4 w-4 animate-spin" }), " Offerte aanmaken"] })] })] }));
144
+ }
@@ -0,0 +1,32 @@
1
+ export interface WerkbonConceptRegel {
2
+ package_id: string | null;
3
+ description: string;
4
+ unit: string;
5
+ quantity: number;
6
+ unit_price_cents: number;
7
+ }
8
+ export interface WerkbonConcept {
9
+ customer_id: string | null;
10
+ customer_name: string;
11
+ parent_work_order_id: string | null;
12
+ parent_work_order_number: string;
13
+ work_address: string;
14
+ room_number: string;
15
+ reference: string;
16
+ complaint_description: string;
17
+ technician_task_description: string;
18
+ budget_mandate_cents: number;
19
+ is_follow_up: boolean;
20
+ estimated_duration_minutes: number | null;
21
+ deadline_date: string | null;
22
+ regels: WerkbonConceptRegel[];
23
+ }
24
+ interface Props {
25
+ initialValue: WerkbonConcept;
26
+ saving: boolean;
27
+ onCancel: () => void;
28
+ onCreate: (value: WerkbonConcept) => Promise<void>;
29
+ }
30
+ export declare function WerkbonAanmakenPaneel({ initialValue, saving, onCancel, onCreate }: Props): import("react").JSX.Element;
31
+ export {};
32
+ //# sourceMappingURL=WerkbonAanmakenPaneel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WerkbonAanmakenPaneel.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/WerkbonAanmakenPaneel.tsx"],"names":[],"mappings":"AAaA,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,wBAAwB,EAAE,MAAM,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,2BAA2B,EAAE,MAAM,CAAC;IACpC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AAED,UAAU,KAAK;IACb,YAAY,EAAE,cAAc,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED,wBAAgB,qBAAqB,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,+BAmLxF"}
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, } from "@flowselections/core";
4
+ import { Loader2, Plus, Trash2, Wrench } from "lucide-react";
5
+ export function WerkbonAanmakenPaneel({ initialValue, saving, onCancel, onCreate }) {
6
+ const [value, setValue] = useState(initialValue);
7
+ useEffect(() => setValue(initialValue), [initialValue]);
8
+ const setField = (field, next) => setValue((current) => ({ ...current, [field]: next }));
9
+ const setRegel = (index, patch) => setField("regels", value.regels.map((regel, regelIndex) => regelIndex === index ? { ...regel, ...patch } : regel));
10
+ const veiligeRegels = Array.isArray(value.regels) ? value.regels : [];
11
+ const totaalCents = Math.round(veiligeRegels.reduce((som, r) => som + Number(r.quantity || 0) * Number(r.unit_price_cents || 0), 0));
12
+ const overMandaat = totaalCents > value.budget_mandate_cents;
13
+ const duur = value.estimated_duration_minutes ?? 0;
14
+ const duurUren = Math.floor(duur / 60);
15
+ const duurMinuten = duur % 60;
16
+ const zetDuur = (uren, minuten) => {
17
+ const totaal = Math.max(0, Math.round(uren) * 60 + Math.round(minuten));
18
+ setField("estimated_duration_minutes", totaal > 0 ? totaal : null);
19
+ };
20
+ const duurOntbreekt = duur <= 0;
21
+ const parentOntbreekt = value.is_follow_up && !value.parent_work_order_id;
22
+ const euro = (c) => new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR" }).format(c / 100);
23
+ return (_jsxs(Card, { className: "h-fit min-w-0", children: [_jsxs(CardHeader, { className: "border-b", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-lg", children: [_jsx(Wrench, { className: "h-5 w-5" }), " Nieuwe werkbon"] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Wordt als nieuw aangemaakt. Inplannen gebeurt daarna in Werkbonnen." })] }), _jsxs(CardContent, { className: "space-y-5 pt-5", children: [_jsxs("label", { className: "flex items-center gap-2 rounded-md border p-3 text-sm", children: [_jsx("input", { type: "checkbox", className: "h-4 w-4", checked: value.is_follow_up, onChange: (event) => setField("is_follow_up", event.target.checked) }), _jsxs("span", { children: [_jsx("span", { className: "font-medium", children: "Vervolgklus" }), _jsx("span", { className: "block text-xs text-muted-foreground", children: value.is_follow_up
24
+ ? `Gekoppeld aan ${value.parent_work_order_number || "de moederwerkbon"}; het nummer loopt door.`
25
+ : "Losse werkbon met een eigen nieuw nummer." })] })] }), _jsxs("section", { className: "space-y-3", children: [_jsx("h3", { className: "text-sm font-semibold", children: "Klant en locatie" }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-klant", children: "Klant" }), _jsx(Input, { id: "werkbon-klant", value: value.customer_name, readOnly: true })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-adres", children: "Werkadres" }), _jsx(Input, { id: "werkbon-adres", value: value.work_address, onChange: (event) => setField("work_address", event.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-ruimte", children: "Kamer / ruimtenummer" }), _jsx(Input, { id: "werkbon-ruimte", value: value.room_number, onChange: (event) => setField("room_number", event.target.value) })] })] }), _jsxs("section", { className: "space-y-3 border-t pt-4", children: [_jsx("h3", { className: "text-sm font-semibold", children: "Project en opdracht" }), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-moeder", children: "Moeder-werkbon" }), _jsx(Input, { id: "werkbon-moeder", value: value.parent_work_order_number, readOnly: true })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-referentie", children: "Referentie" }), _jsx(Input, { id: "werkbon-referentie", value: value.reference, onChange: (event) => setField("reference", event.target.value) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-klacht", children: "Klacht / aanleiding" }), _jsx(Textarea, { id: "werkbon-klacht", rows: 3, value: value.complaint_description, onChange: (event) => setField("complaint_description", event.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-opdracht", children: "Opdracht voor monteur" }), _jsx(Textarea, { id: "werkbon-opdracht", rows: 5, value: value.technician_task_description, onChange: (event) => setField("technician_task_description", event.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-mandaat", children: "Mandaat (\u20AC)" }), _jsx(Input, { id: "werkbon-mandaat", type: "number", min: "0", step: "0.01", value: (value.budget_mandate_cents / 100).toFixed(2), onChange: (event) => setField("budget_mandate_cents", Math.max(0, Math.round(Number(event.target.value || 0) * 100))) })] }), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Duur *" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { "aria-label": "Uren", type: "number", min: "0", value: duurUren, onChange: (e) => zetDuur(Number(e.target.value || 0), duurMinuten) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "u" }), _jsx(Input, { "aria-label": "Minuten", type: "number", min: "0", max: "59", step: "5", value: duurMinuten, onChange: (e) => zetDuur(duurUren, Math.min(59, Number(e.target.value || 0))) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "min" })] }), duurOntbreekt && _jsx("p", { className: "text-xs text-destructive", children: "Duur is verplicht." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "werkbon-deadline", children: "Deadline (optioneel)" }), _jsx(Input, { id: "werkbon-deadline", type: "date", value: value.deadline_date ?? "", onChange: (e) => setField("deadline_date", e.target.value || null) })] })] })] }), _jsxs("section", { className: "space-y-3 border-t pt-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold", children: "Werkbonregels" }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => setField("regels", [...value.regels, { package_id: null, description: "", unit: "st", quantity: 1, unit_price_cents: 0 }]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Regel"] })] }), _jsx("div", { className: "space-y-3", children: value.regels.map((regel, index) => (_jsxs("div", { className: "space-y-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(Textarea, { "aria-label": `Omschrijving regel ${index + 1}`, rows: 2, value: regel.description, onChange: (event) => setRegel(index, { description: event.target.value }), className: "flex-1" }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", "aria-label": `Verwijder regel ${index + 1}`, onClick: () => setField("regels", value.regels.filter((_, i) => i !== index)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Eenheid" }), _jsx(Input, { value: regel.unit, onChange: (event) => setRegel(index, { unit: event.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Aantal" }), _jsx(Input, { type: "number", min: "0", step: "0.01", value: regel.quantity, onChange: (event) => setRegel(index, { quantity: Number(event.target.value || 0) }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Prijs (\u20AC)" }), _jsx(Input, { type: "number", min: "0", step: "0.01", value: (regel.unit_price_cents / 100).toFixed(2), onChange: (event) => setRegel(index, { unit_price_cents: Math.max(0, Math.round(Number(event.target.value || 0) * 100)) }) })] })] })] }, `${regel.package_id ?? "vrij"}-${index}`))) }), _jsxs("div", { className: "space-y-1 rounded-md bg-muted/50 p-3 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Totaal regels" }), _jsx("span", { className: overMandaat ? "font-semibold text-destructive" : "font-semibold", children: euro(totaalCents) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Mandaat" }), _jsx("span", { children: euro(value.budget_mandate_cents) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Nog beschikbaar" }), _jsx("span", { className: overMandaat ? "text-destructive" : "", children: euro(value.budget_mandate_cents - totaalCents) })] }), overMandaat && _jsx("p", { className: "pt-1 text-xs text-destructive", children: "De werkbonregels gaan boven het mandaat uit. Pas de regels aan." })] })] }), _jsxs("div", { className: "flex justify-end gap-2 border-t pt-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: saving, children: "Annuleren" }), _jsxs(Button, { type: "button", onClick: () => void onCreate(value), disabled: saving || parentOntbreekt || overMandaat || duurOntbreekt, children: [saving ? _jsx(Loader2, { className: "mr-1 h-4 w-4 animate-spin" }) : _jsx(Wrench, { className: "mr-1 h-4 w-4" }), "Werkbon aanmaken"] })] })] })] }));
26
+ }
@@ -0,0 +1,4 @@
1
+ export declare function WerkbonVoorstellenLijst({ onOfferteGemaakt }: {
2
+ onOfferteGemaakt?: () => void;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=WerkbonVoorstellenLijst.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WerkbonVoorstellenLijst.d.ts","sourceRoot":"","sources":["../../../src/components/offertes/WerkbonVoorstellenLijst.tsx"],"names":[],"mappings":"AAOA,wBAAgB,uBAAuB,CAAC,EAAE,gBAAgB,EAAE,EAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,+BA8F9F"}
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ import { Badge, Card, CardContent, CardHeader, CardTitle, cn } from "@flowselections/core";
4
+ import { Inbox } from "lucide-react";
5
+ import { useWerkbonVoorstellen } from "../../hooks/useWerkbonVoorstellen";
6
+ import { VoorstelOffertePaneel } from "./VoorstelOffertePaneel";
7
+ import { formatDatum, formatEuro } from "../../lib/offertes";
8
+ export function WerkbonVoorstellenLijst({ onOfferteGemaakt }) {
9
+ const { voorstellen, loading, reload, upsertStatus } = useWerkbonVoorstellen();
10
+ const [actief, setActief] = useState(null);
11
+ const [navigatieLijst, setNavigatieLijst] = useState([]);
12
+ const actiefRef = useRef(null);
13
+ const open = (Array.isArray(voorstellen) ? voorstellen : []).filter((v) => !v.offerte_id);
14
+ // Zodra een voorstel open staat, vries de lijstvolgorde vast: de "gezien"-markering
15
+ // hersorteert de lijst onder water, maar de pijlen en de teller in het paneel moeten
16
+ // blijven wijzen op de posities zoals de gebruiker de lijst zag toen hij opende.
17
+ const openen = async (v) => {
18
+ if (!actiefRef.current)
19
+ setNavigatieLijst(open);
20
+ actiefRef.current = v;
21
+ setActief(v);
22
+ if (!v.seen_at) {
23
+ await upsertStatus(v.id, { seen_at: new Date().toISOString() });
24
+ reload();
25
+ }
26
+ };
27
+ const sluit = () => {
28
+ actiefRef.current = null;
29
+ setActief(null);
30
+ };
31
+ const navigatieBasis = actief ? navigatieLijst : open;
32
+ const actieveIndex = actief
33
+ ? Math.max(navigatieBasis.findIndex((v) => v.id === actief.id), 0)
34
+ : 0;
35
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(Inbox, { className: "h-4 w-4 text-primary" }), " Werkbonvoorstellen uit de mail", _jsxs("span", { className: "text-sm font-normal text-muted-foreground", children: ["(", open.length, ")"] })] }) }), _jsxs(CardContent, { className: "space-y-1", children: [loading && _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }), !loading && open.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen openstaande voorstellen." })), open.map((v) => (_jsxs("button", { type: "button", onClick: () => openen(v), className: cn("flex w-full items-center gap-3 rounded-md border px-3 py-2 text-left hover:bg-muted", !v.seen_at && "border-primary/40 bg-primary/5"), children: [!v.seen_at && _jsx(Badge, { className: "shrink-0", children: "Nieuw" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate font-medium", children: v.subject || v.technician_task_description || "Werkbonvoorstel" }), _jsxs("span", { className: "block truncate text-xs text-muted-foreground", children: [v.klant_naam ?? v.from_name ?? v.from_email ?? "Onbekende klant", " \u00B7", " ", formatDatum(v.reviewed_at ?? v.created_at)] })] }), v.budget_mandate_cents ? (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Mandaat ", formatEuro(v.budget_mandate_cents)] })) : null] }, v.id)))] }), actief && (_jsx(VoorstelOffertePaneel, { voorstel: actief, index: actieveIndex, aantalVoorstellen: navigatieBasis.length, onNavigate: (i) => {
36
+ const doel = navigatieBasis[i];
37
+ if (!doel)
38
+ return;
39
+ const vers = open.find((o) => o.id === doel.id);
40
+ if (vers)
41
+ openen(vers);
42
+ }, onClose: sluit, onSaveConcept: (c) => upsertStatus(actief.id, { ai_concept: c }), onCreated: async (offerteId) => {
43
+ await upsertStatus(actief.id, { offerte_id: offerteId });
44
+ await reload();
45
+ onOfferteGemaakt?.();
46
+ } }))] }));
47
+ }
@@ -0,0 +1,2 @@
1
+ export declare function OfferteEmailSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=OfferteEmailSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteEmailSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/OfferteEmailSettingsCard.tsx"],"names":[],"mappings":"AAWA,wBAAgB,wBAAwB,gCAmDvC"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
4
+ import { Mail } from "lucide-react";
5
+ import { OFFERTE_EMAIL_PURPOSES, bewaarOfferteEmailPurpose, haalMailboxVoorPurpose, haalOfferteEmailPurpose, } from "../../lib/offerteMail";
6
+ export function OfferteEmailSettingsCard() {
7
+ const [purpose, setPurpose] = useState("");
8
+ const [mailbox, setMailbox] = useState(null);
9
+ const [loading, setLoading] = useState(true);
10
+ const laadMailbox = async (p) => {
11
+ try {
12
+ setMailbox(await haalMailboxVoorPurpose(p || null));
13
+ }
14
+ catch {
15
+ setMailbox(null);
16
+ }
17
+ };
18
+ useEffect(() => {
19
+ haalOfferteEmailPurpose()
20
+ .then(async (p) => { setPurpose(p ?? ""); await laadMailbox(p ?? ""); })
21
+ .catch(() => toast.error("E-mailinstelling kon niet worden geladen"))
22
+ .finally(() => setLoading(false));
23
+ }, []);
24
+ const wijzig = async (v) => {
25
+ const vorige = purpose;
26
+ setPurpose(v);
27
+ try {
28
+ await bewaarOfferteEmailPurpose(v);
29
+ await laadMailbox(v);
30
+ toast.success("Doeleinde opgeslagen");
31
+ }
32
+ catch {
33
+ setPurpose(vorige);
34
+ toast.error("Opslaan mislukt");
35
+ }
36
+ };
37
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Offerte e-mail" })] }), _jsx(CardDescription, { children: "Kies via welk e-maildoeleinde uit de e-mailmodule offertes worden verstuurd." })] }), _jsx(CardContent, { className: "space-y-3", children: loading ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "E-maildoeleinde" }), _jsxs(Select, { value: purpose, onValueChange: (v) => void wijzig(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een doeleinde" }) }), _jsx(SelectContent, { children: OFFERTE_EMAIL_PURPOSES.map((o) => _jsx(SelectItem, { value: o.key, children: o.label }, o.key)) })] })] }), purpose ? (mailbox ? (_jsxs("p", { className: "text-sm", children: ["Verzonden vanaf: ", _jsx("strong", { children: mailbox.display_name || mailbox.account_email }), mailbox.display_name && mailbox.account_email ? ` (${mailbox.account_email})` : ""] })) : (_jsx("p", { className: "text-sm text-destructive", children: "Er is geen mailbox aan dit doeleinde gekoppeld. Koppel er een in de e-mailmodule." }))) : _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen doeleinde gekozen; offertes kunnen dan niet worden verstuurd." })] }) })] }));
38
+ }
@@ -0,0 +1,2 @@
1
+ export declare function OfferteHuisstijlSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=OfferteHuisstijlSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteHuisstijlSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/OfferteHuisstijlSettingsCard.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,4BAA4B,gCA0D3C"}
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Input, Label, supabase, toast } from "@flowselections/core";
4
+ import { Building2, ImagePlus, Loader2, Trash2 } from "lucide-react";
5
+ import { bewaarHuisstijl, haalHuisstijl, huisstijlLogoUrl, legeHuisstijl } from "../../hooks/useHuisstijl";
6
+ const velden = [
7
+ { key: "company_name", label: "Bedrijfsnaam" }, { key: "address", label: "Adres" },
8
+ { key: "postal_code", label: "Postcode" }, { key: "city", label: "Plaats" },
9
+ { key: "chamber_of_commerce_number", label: "KvK-nummer" },
10
+ { key: "vat_identification_number", label: "Btw-identificatienummer" },
11
+ { key: "email", label: "E-mailadres", type: "email", optional: true },
12
+ { key: "phone", label: "Telefoonnummer", optional: true }, { key: "iban", label: "IBAN" },
13
+ { key: "website", label: "Website", optional: true },
14
+ { key: "payment_term_days", label: "Standaard betaaltermijn (dagen)", type: "number" },
15
+ ];
16
+ export function OfferteHuisstijlSettingsCard() {
17
+ const [value, setValue] = useState(legeHuisstijl());
18
+ const [logoUrl, setLogoUrl] = useState(null);
19
+ const [loading, setLoading] = useState(true);
20
+ const [fout, setFout] = useState(null);
21
+ const [saving, setSaving] = useState(false);
22
+ useEffect(() => {
23
+ haalHuisstijl()
24
+ .then(async ({ huisstijl }) => {
25
+ setValue(huisstijl);
26
+ setLogoUrl(await huisstijlLogoUrl(huisstijl.logo_path));
27
+ })
28
+ .catch(() => setFout("De huisstijl kon niet worden geladen. Is de Facturen-module in deze omgeving geïnstalleerd?"))
29
+ .finally(() => setLoading(false));
30
+ }, []);
31
+ const uploadLogo = async (file) => {
32
+ if (!file)
33
+ return;
34
+ if (!["image/png", "image/jpeg", "image/webp", "image/svg+xml"].includes(file.type) || file.size > 2 * 1024 * 1024) {
35
+ toast.error("Gebruik PNG, JPEG, WebP of SVG van maximaal 2 MB");
36
+ return;
37
+ }
38
+ const ext = file.name.split(".").pop()?.toLowerCase() || "png";
39
+ const path = `offertes-installatietechniek/${crypto.randomUUID()}.${ext}`;
40
+ const { error } = await supabase.storage.from("layout-logos").upload(path, file, { contentType: file.type, upsert: false });
41
+ if (error) {
42
+ toast.error("Logo uploaden mislukt");
43
+ return;
44
+ }
45
+ setValue((v) => ({ ...v, logo_path: path }));
46
+ setLogoUrl(await huisstijlLogoUrl(path));
47
+ };
48
+ const save = async () => {
49
+ if (value.company_name.toUpperCase().replace(/[^A-Z]/g, "").length < 2) {
50
+ toast.error("De bedrijfsnaam moet minimaal twee letters bevatten");
51
+ return;
52
+ }
53
+ setSaving(true);
54
+ try {
55
+ setValue(await bewaarHuisstijl(value));
56
+ toast.success("Huisstijl opgeslagen");
57
+ }
58
+ catch {
59
+ toast.error("Huisstijl opslaan mislukt");
60
+ }
61
+ finally {
62
+ setSaving(false);
63
+ }
64
+ };
65
+ return _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Building2, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Offertehuisstijl" })] }), _jsx(CardDescription, { children: "Bedrijfsgegevens, logo en kleuren voor offertes. Deze gegevens worden gedeeld met de facturen." })] }), _jsx(CardContent, { className: "space-y-5", children: loading ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }) : fout ? _jsx("p", { className: "text-sm text-destructive", children: fout }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bedrijfslogo" }), _jsxs("div", { className: "flex min-h-28 items-center gap-4 rounded-md border p-3", children: [_jsx("div", { className: "flex h-20 w-36 items-center justify-center bg-muted", children: logoUrl ? _jsx("img", { src: logoUrl, alt: "Bedrijfslogo", className: "max-h-20 max-w-36 object-contain" }) : _jsx(ImagePlus, { className: "h-7 w-7 text-muted-foreground" }) }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", variant: "outline", asChild: true, children: _jsxs("label", { children: ["Afbeelding kiezen", _jsx("input", { className: "sr-only", type: "file", accept: "image/png,image/jpeg,image/webp,image/svg+xml", onChange: (e) => void uploadLogo(e.target.files?.[0]) })] }) }), value.logo_path ? _jsx(Button, { type: "button", variant: "ghost", size: "icon", title: "Logo verwijderen", onClick: () => { setValue((v) => ({ ...v, logo_path: null })); setLogoUrl(null); }, children: _jsx(Trash2, { className: "h-4 w-4" }) }) : null] })] })] }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: velden.map((f) => _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `huisstijl-${f.key}`, children: [f.label, f.optional ? " (optioneel)" : ""] }), _jsx(Input, { id: `huisstijl-${f.key}`, type: f.type || "text", min: f.type === "number" ? 1 : undefined, max: f.type === "number" ? 365 : undefined, value: String(value[f.key] ?? ""), onChange: (e) => setValue((v) => ({ ...v, [f.key]: f.type === "number" ? Number(e.target.value) : e.target.value })) }), f.key === "company_name" ? _jsxs("label", { htmlFor: "huisstijl-show-company-name", className: "flex cursor-pointer items-center gap-2 pt-0.5 text-xs text-muted-foreground", children: [_jsx(Checkbox, { id: "huisstijl-show-company-name", checked: value.show_company_name !== false, onCheckedChange: (checked) => setValue((v) => ({ ...v, show_company_name: checked === true })) }), "Bedrijfsnaam naast het logo tonen in de bovenbalk"] }) : null] }, f.key)) }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: ["primary_color", "secondary_color"].map((key, i) => _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `huisstijl-${key}`, children: ["Bedrijfskleur ", i + 1] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { className: "h-10 w-14 p-1", id: `huisstijl-${key}`, type: "color", value: value[key], onChange: (e) => setValue((v) => ({ ...v, [key]: e.target.value.toUpperCase() })) }), _jsx(Input, { value: value[key], onChange: (e) => setValue((v) => ({ ...v, [key]: e.target.value.toUpperCase() })) })] })] }, key)) }), _jsxs("div", { className: "overflow-hidden rounded-md border", children: [_jsx("div", { className: "h-12 px-4 py-3 text-sm font-semibold text-primary-foreground", style: { backgroundColor: value.primary_color }, children: value.show_company_name !== false ? (value.company_name || "Bedrijfsnaam") : "Logo" }), _jsx("div", { className: "h-8 px-4 py-2 text-xs text-primary-foreground", style: { backgroundColor: value.secondary_color }, children: "KvK \u00B7 Btw-id \u00B7 IBAN" })] }), _jsxs(Button, { onClick: save, disabled: saving, children: [saving ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, "Opslaan"] })] }) })] });
66
+ }
@@ -0,0 +1,2 @@
1
+ export declare function OfferteVoorwaardenSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=OfferteVoorwaardenSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfferteVoorwaardenSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/OfferteVoorwaardenSettingsCard.tsx"],"names":[],"mappings":"AAOA,wBAAgB,8BAA8B,gCAmD7C"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Label, Textarea, toast, } from "@flowselections/core";
4
+ import { ScrollText } from "lucide-react";
5
+ import { bewaarOfferteVoorwaarden, haalOfferteVoorwaarden } from "../../lib/offerteMail";
6
+ export function OfferteVoorwaardenSettingsCard() {
7
+ const [tekst, setTekst] = useState("");
8
+ const [loading, setLoading] = useState(true);
9
+ const [saving, setSaving] = useState(false);
10
+ useEffect(() => {
11
+ haalOfferteVoorwaarden()
12
+ .then(setTekst)
13
+ .catch(() => toast.error("Voorwaarden konden niet worden geladen"))
14
+ .finally(() => setLoading(false));
15
+ }, []);
16
+ const opslaan = async () => {
17
+ setSaving(true);
18
+ try {
19
+ await bewaarOfferteVoorwaarden(tekst);
20
+ toast.success("Voorwaarden opgeslagen");
21
+ }
22
+ catch {
23
+ toast.error("Opslaan mislukt");
24
+ }
25
+ finally {
26
+ setSaving(false);
27
+ }
28
+ };
29
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ScrollText, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Voorwaarden en toelichting" })] }), _jsx(CardDescription, { children: "Deze tekst staat standaard op elke nieuwe offerte, onder het financi\u00EBle overzicht. Per offerte kun je de tekst nog aanpassen." })] }), _jsx(CardContent, { className: "space-y-3", children: loading ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Standaardtekst op de offerte" }), _jsx(Textarea, { rows: 8, value: tekst, onChange: (e) => setTekst(e.target.value), placeholder: "Bijvoorbeeld:\n- Deze offerte is 30 dagen geldig.\n- Wijzigingen in de uitvoering kunnen de prijs beïnvloeden.\n- Betaling binnen 14 dagen na factuurdatum." })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Leeg laten = geen standaardtekst op nieuwe offertes. Al verzonden offertes veranderen niet." }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: () => void opslaan(), disabled: saving, children: saving ? "Opslaan…" : "Opslaan" }) })] }) })] }));
30
+ }
@@ -0,0 +1,32 @@
1
+ export interface ExtraWerkMelding {
2
+ offerte_id: string;
3
+ offerte_number: string | null;
4
+ omschrijving: string | null;
5
+ notities: string | null;
6
+ mandaat_restant_cents: number | null;
7
+ vat_rate: number | null;
8
+ valid_until: string | null;
9
+ created_at: string;
10
+ customer_id: string | null;
11
+ klant_naam: string | null;
12
+ geschat_cents: number;
13
+ werkbon: Record<string, any> | null;
14
+ seen_at: string | null;
15
+ ai_concept: unknown;
16
+ }
17
+ export interface BronRegel {
18
+ id: string;
19
+ soort: string;
20
+ description: string;
21
+ quantity: number | null;
22
+ unit: string | null;
23
+ }
24
+ export declare function useExtraWerkMeldingen(): {
25
+ meldingen: ExtraWerkMelding[];
26
+ loading: boolean;
27
+ reload: () => Promise<void>;
28
+ logEvent: (offerteId: string, eventType: string, omschrijving: string, metadata?: Record<string, unknown>) => Promise<void>;
29
+ };
30
+ /** Materialen en pakketten die al op de bronwerkbon staan. */
31
+ export declare function laadBronwerkbonRegels(werkbonId: string | null | undefined): Promise<BronRegel[]>;
32
+ //# sourceMappingURL=useExtraWerkMeldingen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExtraWerkMeldingen.d.ts","sourceRoot":"","sources":["../../src/hooks/useExtraWerkMeldingen.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACpC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,wBAAgB,qBAAqB;;;;0BAoEf,MAAM,aAAa,MAAM,gBAAgB,MAAM,aAAa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;EAUxG;AAED,8DAA8D;AAC9D,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAkBtG"}
@@ -0,0 +1,103 @@
1
+ // ============================================================================
2
+ // src/hooks/useExtraWerkMeldingen.ts — extra-werkmeldingen van monteurs (boven
3
+ // mandaat). De monteur-app maakt daarvoor een concept-offerte in itk_offertes;
4
+ // wij lezen die plus de bronwerkbon. Gezien/uitgewerkt via itk_offerte_events.
5
+ // ============================================================================
6
+ import { useCallback, useEffect, useState } from "react";
7
+ import { supabase, toast } from "@flowselections/core";
8
+ const db = supabase;
9
+ export function useExtraWerkMeldingen() {
10
+ const [meldingen, setMeldingen] = useState([]);
11
+ const [loading, setLoading] = useState(true);
12
+ const load = useCallback(async () => {
13
+ setLoading(true);
14
+ try {
15
+ const { data, error } = await db
16
+ .from("itk_offertes")
17
+ .select("*")
18
+ .eq("source_type", "work_order")
19
+ .eq("status", "concept")
20
+ .ilike("notities", "Extra werk gemeld%")
21
+ .order("created_at", { ascending: false });
22
+ if (error)
23
+ throw error;
24
+ const rows = Array.isArray(data) ? data : [];
25
+ const ids = rows.map((r) => r.id);
26
+ const wbIds = [...new Set(rows.map((r) => r.source_ref).filter(Boolean))];
27
+ const custIds = [...new Set(rows.map((r) => r.customer_id).filter(Boolean))];
28
+ const [evRes, wbRes, klRes, rgRes] = await Promise.all([
29
+ ids.length
30
+ ? db.from("itk_offerte_events").select("offerte_id, event_type, created_at, metadata").in("offerte_id", ids).in("event_type", ["melding_bekeken", "melding_uitgewerkt", "melding_ai_concept"]).order("created_at", { ascending: false })
31
+ : Promise.resolve({ data: [] }),
32
+ wbIds.length ? db.from("werkbonnen").select("*").in("id", wbIds) : Promise.resolve({ data: [] }),
33
+ custIds.length ? db.from("itk_customers").select("id, name").in("id", custIds) : Promise.resolve({ data: [] }),
34
+ ids.length ? db.from("itk_offerte_regels").select("offerte_id, quantity, unit_price_cents").in("offerte_id", ids) : Promise.resolve({ data: [] }),
35
+ ]);
36
+ const events = Array.isArray(evRes.data) ? evRes.data : [];
37
+ const werkbonnen = Array.isArray(wbRes.data) ? wbRes.data : [];
38
+ const klanten = Array.isArray(klRes.data) ? klRes.data : [];
39
+ const regels = Array.isArray(rgRes.data) ? rgRes.data : [];
40
+ const lijst = rows
41
+ .filter((r) => !events.some((e) => e.offerte_id === r.id && e.event_type === "melding_uitgewerkt"))
42
+ .map((r) => ({
43
+ offerte_id: r.id,
44
+ offerte_number: r.offerte_number ?? null,
45
+ omschrijving: r.omschrijving ?? null,
46
+ notities: r.notities ?? null,
47
+ mandaat_restant_cents: r.mandaat_restant_cents ?? null,
48
+ vat_rate: r.vat_rate ?? null,
49
+ valid_until: r.valid_until ?? null,
50
+ created_at: r.created_at,
51
+ customer_id: r.customer_id ?? null,
52
+ klant_naam: klanten.find((k) => k.id === r.customer_id)?.name ?? null,
53
+ geschat_cents: regels
54
+ .filter((g) => g.offerte_id === r.id)
55
+ .reduce((s, g) => s + Math.round(Number(g.quantity || 0) * Number(g.unit_price_cents || 0)), 0),
56
+ werkbon: werkbonnen.find((w) => w.id === r.source_ref) ?? null,
57
+ seen_at: events.find((e) => e.offerte_id === r.id && e.event_type === "melding_bekeken")?.created_at ?? null,
58
+ ai_concept: events.find((e) => e.offerte_id === r.id && e.event_type === "melding_ai_concept")?.metadata?.concept ?? null,
59
+ }));
60
+ lijst.sort((a, b) => Number(!!a.seen_at) - Number(!!b.seen_at));
61
+ setMeldingen(lijst);
62
+ }
63
+ catch {
64
+ toast.error("Extra-werkmeldingen konden niet worden geladen");
65
+ setMeldingen([]);
66
+ }
67
+ finally {
68
+ setLoading(false);
69
+ }
70
+ }, []);
71
+ useEffect(() => {
72
+ load();
73
+ }, [load]);
74
+ const logEvent = useCallback(async (offerteId, eventType, omschrijving, metadata) => {
75
+ const { error } = await db
76
+ .from("itk_offerte_events")
77
+ .insert({ offerte_id: offerteId, event_type: eventType, omschrijving, metadata: metadata ?? {} });
78
+ if (error)
79
+ console.error("melding-event opslaan mislukt", error);
80
+ }, []);
81
+ return { meldingen, loading, reload: load, logEvent };
82
+ }
83
+ /** Materialen en pakketten die al op de bronwerkbon staan. */
84
+ export async function laadBronwerkbonRegels(werkbonId) {
85
+ if (!werkbonId)
86
+ return [];
87
+ const [mRes, pRes] = await Promise.all([
88
+ db.from("werkbon_materials").select("id, description, quantity, unit").eq("werkbon_id", werkbonId),
89
+ db.from("werkbon_package_lines").select("id, quantity, werkbon_packages(name, code)").eq("werkbon_id", werkbonId),
90
+ ]);
91
+ const mat = Array.isArray(mRes.data) ? mRes.data : [];
92
+ const pak = Array.isArray(pRes.data) ? pRes.data : [];
93
+ return [
94
+ ...pak.map((p) => ({
95
+ id: p.id,
96
+ soort: "pakket",
97
+ description: p.werkbon_packages?.name ?? p.werkbon_packages?.code ?? "Pakket",
98
+ quantity: p.quantity ?? null,
99
+ unit: null,
100
+ })),
101
+ ...mat.map((m) => ({ id: m.id, soort: "materiaal", description: m.description ?? "", quantity: m.quantity ?? null, unit: m.unit ?? null })),
102
+ ];
103
+ }