@flowselections/facturen-installatie-techniek 1.0.2 → 1.3.4
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/dist-lib/components/ArticlePhoto.d.ts +8 -0
- package/dist-lib/components/ArticlePhoto.d.ts.map +1 -0
- package/dist-lib/components/ArticlePhoto.js +7 -0
- package/dist-lib/components/FacturenForInstallatieTechniek.d.ts.map +1 -1
- package/dist-lib/components/FacturenForInstallatieTechniek.js +143 -50
- package/dist-lib/components/FactuurDetailPage.d.ts.map +1 -1
- package/dist-lib/components/FactuurDetailPage.js +224 -13
- package/dist-lib/components/InvoiceA4Preview.d.ts +19 -0
- package/dist-lib/components/InvoiceA4Preview.d.ts.map +1 -0
- package/dist-lib/components/InvoiceA4Preview.js +118 -0
- package/dist-lib/components/InvoiceDetailsFields.d.ts +7 -0
- package/dist-lib/components/InvoiceDetailsFields.d.ts.map +1 -0
- package/dist-lib/components/InvoiceDetailsFields.js +21 -0
- package/dist-lib/components/WerkbonPreviewPanel.d.ts +4 -0
- package/dist-lib/components/WerkbonPreviewPanel.d.ts.map +1 -0
- package/dist-lib/components/WerkbonPreviewPanel.js +40 -0
- package/dist-lib/components/settings/InstellingenPage.d.ts +5 -0
- package/dist-lib/components/settings/InstellingenPage.d.ts.map +1 -0
- package/dist-lib/components/settings/InstellingenPage.js +119 -0
- package/dist-lib/components/settings/InvoiceBrandingSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/InvoiceBrandingSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/InvoiceBrandingSettingsCard.js +64 -0
- package/dist-lib/components/settings/InvoiceEmailSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/InvoiceEmailSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/InvoiceEmailSettingsCard.js +41 -0
- package/dist-lib/index.d.ts +8 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +14 -2
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +11 -22550
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +3 -1
- package/dist-lib/integrations/supabase/client.d.ts +2 -22547
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.js +4 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2 -22547
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/previewAuthStorage.js +99 -0
- package/dist-lib/integrations/supabase/types.d.ts +24631 -11349
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +2 -0
- package/dist-lib/lib/articles.functions.d.ts +19 -0
- package/dist-lib/lib/articles.functions.d.ts.map +1 -0
- package/dist-lib/lib/articles.functions.js +91 -0
- package/dist-lib/lib/config.d.ts +16 -0
- package/dist-lib/lib/config.d.ts.map +1 -0
- package/dist-lib/lib/config.js +17 -0
- package/dist-lib/lib/facturen.d.ts +196 -0
- package/dist-lib/lib/facturen.d.ts.map +1 -1
- package/dist-lib/lib/facturen.js +441 -16
- package/package.json +57 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ArticlePhoto.d.ts","sourceRoot":"","sources":["../../src/components/ArticlePhoto.tsx"],"names":[],"mappings":"AAEA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,SAAuB,EAAE,EAAE,iBAAiB,+BAcpF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
export function ArticlePhoto({ src, alt, className = "h-14 w-14" }) {
|
|
4
|
+
const [failed, setFailed] = useState(false);
|
|
5
|
+
useEffect(() => setFailed(false), [src]);
|
|
6
|
+
return (_jsx("div", { className: `shrink-0 overflow-hidden rounded-sm bg-muted ${className}`, children: src && !failed ? (_jsx("img", { src: src, alt: alt, className: "h-full w-full object-cover", onError: () => setFailed(true) })) : (_jsx("div", { className: "h-full w-full", role: "img", "aria-label": `Geen foto beschikbaar voor ${alt}` })) }));
|
|
7
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AAgJA,wBAAgB,8BAA8B,gCAyd7C"}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { WerkbonPreviewPanel } from "../components/WerkbonPreviewPanel";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import {
|
|
4
|
+
import { DEFAULT_VAT_RATE } from "../lib/config";
|
|
5
|
+
import { supabase } from "@flowselections/core";
|
|
6
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
4
7
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
5
|
-
import { Plus
|
|
6
|
-
import { createInvoiceFromSource,
|
|
7
|
-
|
|
8
|
+
import { Plus } from "lucide-react";
|
|
9
|
+
import { createInvoiceFromSource, emptyInvoiceDetails, fetchInvoicesDetailed, fetchInvoiceKpis, fetchSourceLines, formatCents, listSources, SOURCE_LABELS, STATUS_LABELS, emptyInvoiceBranding, fetchInvoiceBranding, getInvoiceLogoUrl, editLineToBillable, emptyEditLine, peekNextInvoiceNumber, } from "../lib/facturen";
|
|
10
|
+
import { InvoiceA4Preview } from "./InvoiceA4Preview";
|
|
8
11
|
function statusVariant(status) {
|
|
9
12
|
if (status === "betaald")
|
|
10
13
|
return "default";
|
|
@@ -14,11 +17,26 @@ function statusVariant(status) {
|
|
|
14
17
|
return "outline";
|
|
15
18
|
return "secondary";
|
|
16
19
|
}
|
|
20
|
+
function InvoiceTable({ invoices, emptyText, onOpen, }) {
|
|
21
|
+
if (invoices.length === 0) {
|
|
22
|
+
return _jsx("p", { className: "text-sm text-muted-foreground", children: emptyText });
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Nummer" }), _jsx(TableHead, { children: "Klant" }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { className: "text-right", children: "Totaal" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: invoices.map((invoice) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => onOpen(invoice.id), children: [_jsx(TableCell, { className: "font-medium", children: _jsx(Link, { to: "/facturen-installatietechniek/detail/$id", params: { id: invoice.id }, search: { send: false }, className: "underline-offset-2 hover:underline", children: invoice.invoice_number ?? "—" }) }), _jsx(TableCell, { children: invoice.customer_name ?? "—" }), _jsx(TableCell, { children: _jsx(Badge, { variant: "outline", children: SOURCE_LABELS[invoice.source_type]
|
|
25
|
+
? SOURCE_LABELS[invoice.source_type] + (invoice.source_number ? ` ${invoice.source_number}` : "")
|
|
26
|
+
: "—" }) }), _jsx(TableCell, { children: new Date(invoice.invoice_date).toLocaleDateString("nl-NL") }), _jsx(TableCell, { className: "text-right", children: formatCents(invoice.total_cents) }), _jsx(TableCell, { children: _jsx(Badge, { variant: statusVariant(invoice.status), children: STATUS_LABELS[invoice.status] ?? invoice.status }) })] }, invoice.id))) })] }));
|
|
27
|
+
}
|
|
17
28
|
export function FacturenForInstallatieTechniek() {
|
|
29
|
+
const navigate = useNavigate();
|
|
18
30
|
const [invoices, setInvoices] = useState([]);
|
|
19
31
|
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [sessionExpired, setSessionExpired] = useState(false);
|
|
33
|
+
const [loadError, setLoadError] = useState(false);
|
|
34
|
+
const [hasMore, setHasMore] = useState(false);
|
|
35
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
36
|
+
const [kpiData, setKpiData] = useState(null);
|
|
20
37
|
const [statusFilter, setStatusFilter] = useState("alle");
|
|
21
38
|
const [sourceFilter, setSourceFilter] = useState("alle");
|
|
39
|
+
const [paidPeriod, setPaidPeriod] = useState("maand");
|
|
22
40
|
const [open, setOpen] = useState(false);
|
|
23
41
|
const [saving, setSaving] = useState(false);
|
|
24
42
|
// bron-tab
|
|
@@ -27,22 +45,83 @@ export function FacturenForInstallatieTechniek() {
|
|
|
27
45
|
const [sourcesLoading, setSourcesLoading] = useState(false);
|
|
28
46
|
const [selectedSource, setSelectedSource] = useState("");
|
|
29
47
|
// handmatig-tab
|
|
30
|
-
const [
|
|
31
|
-
const [
|
|
32
|
-
const [
|
|
33
|
-
const [
|
|
48
|
+
const [pickedCustomerId, setPickedCustomerId] = useState(null);
|
|
49
|
+
const [tab, setTab] = useState("handmatig");
|
|
50
|
+
const [details, setDetails] = useState(emptyInvoiceDetails);
|
|
51
|
+
const [vatRate, setVatRate] = useState(String(DEFAULT_VAT_RATE));
|
|
52
|
+
const [lines, setLines] = useState([emptyEditLine()]);
|
|
53
|
+
const [nextNumber, setNextNumber] = useState(null);
|
|
54
|
+
const [linkedWorkOrder, setLinkedWorkOrder] = useState(null);
|
|
55
|
+
const [branding, setBranding] = useState(emptyInvoiceBranding);
|
|
56
|
+
const [logoUrl, setLogoUrl] = useState(null);
|
|
57
|
+
const resetForm = () => {
|
|
58
|
+
setPickedCustomerId(null);
|
|
59
|
+
setDetails(emptyInvoiceDetails(branding.payment_term_days));
|
|
60
|
+
setLines([emptyEditLine(vatRate)]);
|
|
61
|
+
setLinkedWorkOrder(null);
|
|
62
|
+
};
|
|
63
|
+
const openNew = () => {
|
|
64
|
+
resetForm();
|
|
65
|
+
setTab("handmatig");
|
|
66
|
+
setOpen(true);
|
|
67
|
+
};
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!open)
|
|
70
|
+
return;
|
|
71
|
+
fetchInvoiceBranding().then(async (row) => {
|
|
72
|
+
setBranding(row);
|
|
73
|
+
setLogoUrl(await getInvoiceLogoUrl(row.logo_path));
|
|
74
|
+
setDetails((current) => ({
|
|
75
|
+
...current,
|
|
76
|
+
payment_term_days: current.payment_term_days === "30" ? String(row.payment_term_days) : current.payment_term_days,
|
|
77
|
+
}));
|
|
78
|
+
}).catch(() => toast.error("Factuurhuisstijl kon niet worden geladen"));
|
|
79
|
+
peekNextInvoiceNumber().then(setNextNumber);
|
|
80
|
+
}, [open]);
|
|
34
81
|
const load = useCallback(async () => {
|
|
35
82
|
setLoading(true);
|
|
36
83
|
try {
|
|
37
|
-
|
|
84
|
+
const { data: userData } = await supabase.auth.getUser();
|
|
85
|
+
if (!userData?.user) {
|
|
86
|
+
setSessionExpired(true);
|
|
87
|
+
setLoadError(false);
|
|
88
|
+
setInvoices([]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setSessionExpired(false);
|
|
92
|
+
const result = await fetchInvoicesDetailed({ status: statusFilter, source: sourceFilter });
|
|
93
|
+
setLoadError(result.failed);
|
|
94
|
+
setInvoices(result.failed ? [] : result.invoices);
|
|
95
|
+
setHasMore(result.hasMore);
|
|
96
|
+
fetchInvoiceKpis().then(setKpiData).catch(() => setKpiData(null));
|
|
38
97
|
}
|
|
39
98
|
catch {
|
|
40
|
-
|
|
99
|
+
setLoadError(true);
|
|
100
|
+
setInvoices([]);
|
|
41
101
|
}
|
|
42
102
|
finally {
|
|
43
103
|
setLoading(false);
|
|
44
104
|
}
|
|
45
|
-
}, []);
|
|
105
|
+
}, [statusFilter, sourceFilter]);
|
|
106
|
+
const loadMore = useCallback(async () => {
|
|
107
|
+
setLoadingMore(true);
|
|
108
|
+
try {
|
|
109
|
+
const offset = invoices.filter((i) => i.status !== "concept").length;
|
|
110
|
+
const result = await fetchInvoicesDetailed({ offset, status: statusFilter, source: sourceFilter });
|
|
111
|
+
if (result.failed) {
|
|
112
|
+
toast.error("Meer facturen laden is mislukt");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setInvoices((prev) => {
|
|
116
|
+
const seen = new Set(prev.map((i) => i.id));
|
|
117
|
+
return [...prev, ...result.invoices.filter((i) => !seen.has(i.id))];
|
|
118
|
+
});
|
|
119
|
+
setHasMore(result.hasMore);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
setLoadingMore(false);
|
|
123
|
+
}
|
|
124
|
+
}, [invoices, statusFilter, sourceFilter]);
|
|
46
125
|
useEffect(() => {
|
|
47
126
|
void load();
|
|
48
127
|
}, [load]);
|
|
@@ -68,20 +147,34 @@ export function FacturenForInstallatieTechniek() {
|
|
|
68
147
|
}, [open, sourceType]);
|
|
69
148
|
const filtered = useMemo(() => invoices.filter((i) => (statusFilter === "alle" || i.status === statusFilter) &&
|
|
70
149
|
(sourceFilter === "alle" || i.source_type === sourceFilter)), [invoices, statusFilter, sourceFilter]);
|
|
150
|
+
const conceptInvoices = useMemo(() => filtered.filter((i) => i.status === "concept"), [filtered]);
|
|
151
|
+
const sendOrPaidInvoices = useMemo(() => filtered.filter((i) => i.status !== "concept"), [filtered]);
|
|
152
|
+
const openInvoice = useCallback((id) => navigate({
|
|
153
|
+
to: "/facturen-installatietechniek/detail/$id",
|
|
154
|
+
params: { id },
|
|
155
|
+
search: { send: false },
|
|
156
|
+
}), [navigate]);
|
|
71
157
|
const kpi = useMemo(() => {
|
|
72
158
|
const now = new Date();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
159
|
+
const inPeriod = (d) => {
|
|
160
|
+
if (paidPeriod === "jaar")
|
|
161
|
+
return d.getFullYear() === now.getFullYear();
|
|
162
|
+
if (paidPeriod === "maand")
|
|
163
|
+
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
|
|
164
|
+
// huidige kalenderweek (maandag t/m zondag)
|
|
165
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
166
|
+
start.setDate(start.getDate() - ((start.getDay() + 6) % 7));
|
|
167
|
+
const end = new Date(start);
|
|
168
|
+
end.setDate(end.getDate() + 7);
|
|
169
|
+
return d >= start && d < end;
|
|
170
|
+
};
|
|
171
|
+
const paidRows = Array.isArray(kpiData?.paidThisYear) ? kpiData.paidThisYear : [];
|
|
172
|
+
const paidMonthCents = paidRows
|
|
173
|
+
.filter((i) => inPeriod(new Date(i.invoice_date)))
|
|
174
|
+
.reduce((sum, i) => sum + (Number(i.total_cents) || 0), 0);
|
|
175
|
+
const openCount = kpiData?.openCount ?? 0;
|
|
176
|
+
return { paidMonthCents, openCount };
|
|
177
|
+
}, [kpiData, paidPeriod]);
|
|
85
178
|
const createFromSource = async () => {
|
|
86
179
|
const source = sources.find((s) => s.id === selectedSource);
|
|
87
180
|
if (!source) {
|
|
@@ -115,40 +208,29 @@ export function FacturenForInstallatieTechniek() {
|
|
|
115
208
|
}
|
|
116
209
|
};
|
|
117
210
|
const createManual = async () => {
|
|
118
|
-
const payloadLines = lines
|
|
119
|
-
|
|
120
|
-
.map((l, index) => {
|
|
121
|
-
const quantity = parseFloat(l.quantity.replace(",", ".")) || 0;
|
|
122
|
-
const unitPrice = euroToCents(l.price);
|
|
123
|
-
return {
|
|
124
|
-
description: l.description.trim(),
|
|
125
|
-
kind: "other",
|
|
126
|
-
quantity,
|
|
127
|
-
unit: l.unit || "stuk",
|
|
128
|
-
unit_price_cents: unitPrice,
|
|
129
|
-
line_total_cents: Math.round(quantity * unitPrice),
|
|
130
|
-
sort_order: index + 1,
|
|
131
|
-
};
|
|
132
|
-
});
|
|
133
|
-
if (!customerName.trim() || payloadLines.length === 0) {
|
|
211
|
+
const payloadLines = lines.filter((l) => l.description.trim() !== "").map(editLineToBillable);
|
|
212
|
+
if (!details.customer_name.trim() || payloadLines.length === 0) {
|
|
134
213
|
toast.error("Vul een klantnaam en minimaal één regel in");
|
|
135
214
|
return;
|
|
136
215
|
}
|
|
137
216
|
setSaving(true);
|
|
138
217
|
try {
|
|
139
218
|
const result = await createInvoiceFromSource({
|
|
140
|
-
sourceType: "manual",
|
|
141
|
-
sourceId: null,
|
|
142
|
-
|
|
143
|
-
vatRate: parseFloat(vatRate.replace(",", ".")) ||
|
|
144
|
-
invoiceDate,
|
|
219
|
+
sourceType: linkedWorkOrder ? "work_order" : "manual",
|
|
220
|
+
sourceId: linkedWorkOrder?.id ?? null,
|
|
221
|
+
customerId: linkedWorkOrder?.customer_id ?? pickedCustomerId ?? null,
|
|
222
|
+
vatRate: parseFloat(vatRate.replace(",", ".")) || DEFAULT_VAT_RATE,
|
|
145
223
|
lines: payloadLines,
|
|
224
|
+
details,
|
|
146
225
|
});
|
|
147
|
-
toast.success(`Factuur ${result.invoice_number} aangemaakt`);
|
|
148
|
-
|
|
149
|
-
setLines([emptyLine()]);
|
|
226
|
+
toast.success(result.reused ? `Bestaand concept ${result.invoice_number}` : `Factuur ${result.invoice_number} aangemaakt`);
|
|
227
|
+
resetForm();
|
|
150
228
|
setOpen(false);
|
|
151
|
-
await
|
|
229
|
+
await navigate({
|
|
230
|
+
to: "/facturen-installatietechniek/detail/$id",
|
|
231
|
+
params: { id: result.id },
|
|
232
|
+
search: { send: true },
|
|
233
|
+
});
|
|
152
234
|
}
|
|
153
235
|
catch {
|
|
154
236
|
toast.error("Factuur aanmaken mislukt");
|
|
@@ -157,5 +239,16 @@ export function FacturenForInstallatieTechniek() {
|
|
|
157
239
|
setSaving(false);
|
|
158
240
|
}
|
|
159
241
|
};
|
|
160
|
-
return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-
|
|
242
|
+
return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: paidPeriod === "week" ? "Betaald deze week" : paidPeriod === "jaar" ? "Betaald dit jaar" : "Betaald deze maand" }), _jsxs(Select, { value: paidPeriod, onValueChange: (v) => setPaidPeriod(v), children: [_jsx(SelectTrigger, { className: "h-7 w-24 text-xs", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "week", children: "Week" }), _jsx(SelectItem, { value: "maand", children: "Maand" }), _jsx(SelectItem, { value: "jaar", children: "Jaar" })] })] })] }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: formatCents(kpi.paidMonthCents) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Openstaand" }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: kpi.openCount })] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs(Select, { value: statusFilter, onValueChange: setStatusFilter, children: [_jsx(SelectTrigger, { className: "w-44", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle statussen" }), Object.entries(STATUS_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsxs(Select, { value: sourceFilter, onValueChange: setSourceFilter, children: [_jsx(SelectTrigger, { className: "w-44", children: _jsx(SelectValue, { placeholder: "Bron" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle bronnen" }), Object.entries(SOURCE_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: openNew, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Nieuwe factuur"] }) })] }), loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" })) : sessionExpired ? (_jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col items-start gap-3 py-6", children: [_jsx("p", { className: "text-sm", children: "Je sessie is verlopen, log opnieuw in om je facturen te zien." }), _jsx(Button, { asChild: true, children: _jsx(Link, { to: "/login", children: "Opnieuw inloggen" }) })] }) })) : loadError ? (_jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col items-start gap-3 py-6", children: [_jsx("p", { className: "text-sm", children: "Facturen konden niet worden geladen. Probeer het opnieuw." }), _jsx(Button, { onClick: () => void load(), children: "Opnieuw proberen" })] }) })) : (_jsxs(_Fragment, { children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Goedkeuren en verzenden" }) }), _jsx(CardContent, { children: _jsx(InvoiceTable, { invoices: conceptInvoices, emptyText: "Geen conceptfacturen.", onOpen: openInvoice }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Facturen" }) }), _jsxs(CardContent, { children: [_jsx(InvoiceTable, { invoices: sendOrPaidInvoices, emptyText: "Geen facturen gevonden.", onOpen: openInvoice }), hasMore && (_jsx("div", { className: "mt-4 flex justify-center", children: _jsx(Button, { variant: "outline", onClick: () => void loadMore(), disabled: loadingMore, children: loadingMore ? "Laden…" : "Meer laden" }) }))] })] })] })), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "flex max-h-[92vh] max-w-[calc(100vw-2rem)] flex-col sm:max-w-7xl", children: [_jsx(DialogHeader, { className: "shrink-0", children: _jsx(DialogTitle, { children: "Nieuwe factuur" }) }), _jsxs("div", { className: linkedWorkOrder ? "grid min-h-0 flex-1 gap-6 overflow-y-auto md:grid-cols-2 md:overflow-hidden" : "contents", children: [linkedWorkOrder && (_jsx("div", { className: "min-h-0 rounded-md border bg-muted/30 p-4 md:overflow-y-auto", children: _jsx(WerkbonPreviewPanel, { workOrderId: linkedWorkOrder.id }) })), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto pr-1", children: [linkedWorkOrder && _jsx("p", { className: "mb-2 text-xs uppercase tracking-wide text-muted-foreground", children: "Factuur" }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "bron", children: "Uit bron" }), _jsx(TabsTrigger, { value: "handmatig", children: "Handmatig" })] }), _jsxs(TabsContent, { value: "bron", className: "space-y-4 pt-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Brontype" }), _jsxs(Select, { value: sourceType, onValueChange: (v) => setSourceType(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "work_order", children: "Werkbon" }), _jsx(SelectItem, { value: "quotation", children: "Offerte" })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bron" }), sourcesLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Bronnen laden\u2026" })) : sources.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen factureerbare bronnen gevonden." })) : (_jsxs(Select, { value: selectedSource, onValueChange: setSelectedSource, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een bron" }) }), _jsx(SelectContent, { children: sources.map((s) => (_jsx(SelectItem, { value: s.id, children: (s.number ?? "zonder nummer") + (s.customer_name ? ` — ${s.customer_name}` : "") }, s.id))) })] }))] }), _jsx(Button, { onClick: createFromSource, disabled: saving || !selectedSource, className: "w-full", children: saving ? "Bezig…" : "Factuur aanmaken" })] }), _jsxs(TabsContent, { value: "handmatig", className: "space-y-4 pt-4", children: [linkedWorkOrder ? (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Gekoppeld aan werkbon ", _jsx("span", { className: "font-medium", children: linkedWorkOrder.number ?? "—" })] })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { htmlFor: "btw", className: "text-xs", children: "Standaard btw % nieuwe regels" }), _jsx(Input, { id: "btw", className: "h-8 w-20", value: vatRate, onChange: (e) => setVatRate(e.target.value) })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Klik op de teksten in de factuur om ze aan te passen." }), _jsx(InvoiceA4Preview, { branding: branding, details: details, lines: lines, invoiceNumber: nextNumber, numberIsProvisional: true, logoUrl: logoUrl, printable: false, editable: true, defaultVat: vatRate, onDetailsChange: setDetails, onLinesChange: setLines, onCustomerPick: linkedWorkOrder ? undefined : (c) => {
|
|
243
|
+
setPickedCustomerId(c.id);
|
|
244
|
+
setDetails((d) => ({
|
|
245
|
+
...d,
|
|
246
|
+
customer_name: c.name ?? "",
|
|
247
|
+
customer_address: c.billing_street ?? "",
|
|
248
|
+
customer_postal_code: c.billing_postcode ?? "",
|
|
249
|
+
customer_city: c.billing_city ?? "",
|
|
250
|
+
debtor_number: c.customer_number ?? "",
|
|
251
|
+
customer_vat_number: c.vat_number ?? "",
|
|
252
|
+
}));
|
|
253
|
+
} }), _jsx(Button, { onClick: createManual, disabled: saving, className: "w-full", children: saving ? "Bezig…" : "Factuur aanmaken" })] })] })] })] })] }) })] }));
|
|
161
254
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,iBAAiB,gCA+ahC"}
|
|
@@ -1,32 +1,107 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useState } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { DEFAULT_VAT_RATE } from "../lib/config";
|
|
4
|
+
import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
|
5
|
+
import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, toast, } from "@flowselections/core";
|
|
6
|
+
import { ArrowLeft, ChevronLeft, ChevronRight, Download, FileCheck2, Mail, Pencil, Save, X } from "lucide-react";
|
|
7
|
+
import { detailsFromRecord, downloadPdfBase64, elementToPdfBase64, emptyInvoiceBranding, emptyInvoiceDetails, fetchCustomerEmail, fetchInvoice, fetchInvoiceEmailPurpose, fetchSenderForPurpose, fetchInvoiceItems, fetchInvoices, getInvoiceLogoUrl, itemToEditLine, registerInvoiceSend, sendInvoiceEmail, SOURCE_LABELS, STATUS_LABELS, updateInvoiceFull, updateInvoiceStatus, } from "../lib/facturen";
|
|
8
|
+
import { InvoiceA4Preview } from "./InvoiceA4Preview";
|
|
9
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10
|
+
const isValidEmail = (value) => EMAIL_PATTERN.test(value.trim());
|
|
7
11
|
export function FactuurDetailPage() {
|
|
8
12
|
const { id } = useParams({ from: "/_authenticated/facturen-installatietechniek/detail/$id" });
|
|
13
|
+
const search = useSearch({ from: "/_authenticated/facturen-installatietechniek/detail/$id" });
|
|
14
|
+
const navigate = useNavigate();
|
|
9
15
|
const [invoice, setInvoice] = useState(null);
|
|
10
|
-
const [
|
|
16
|
+
const [lines, setLines] = useState([]);
|
|
17
|
+
const [details, setDetails] = useState(emptyInvoiceDetails);
|
|
11
18
|
const [loading, setLoading] = useState(true);
|
|
12
19
|
const [saving, setSaving] = useState(false);
|
|
20
|
+
const [editing, setEditing] = useState(false);
|
|
21
|
+
const [logoUrl, setLogoUrl] = useState(null);
|
|
22
|
+
const [sendOpen, setSendOpen] = useState(false);
|
|
23
|
+
const [mail, setMail] = useState({ to: "", subject: "", message: "" });
|
|
24
|
+
const [mailToTouched, setMailToTouched] = useState(false);
|
|
25
|
+
const [knownEmail, setKnownEmail] = useState("");
|
|
26
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
27
|
+
const [sender, setSender] = useState(null);
|
|
28
|
+
const [attachPdf, setAttachPdf] = useState(true);
|
|
29
|
+
const [preparedPdf, setPreparedPdf] = useState("");
|
|
30
|
+
const [preparingPdf, setPreparingPdf] = useState(false);
|
|
31
|
+
const [siblingInvoices, setSiblingInvoices] = useState(null);
|
|
32
|
+
const [siblingsLoading, setSiblingsLoading] = useState(true);
|
|
33
|
+
const siblingRetryRef = useRef(false);
|
|
34
|
+
const sheetRef = useRef(null);
|
|
35
|
+
const autoOpenedRef = useRef(false);
|
|
36
|
+
const loadSeqRef = useRef(0);
|
|
13
37
|
const load = useCallback(async () => {
|
|
38
|
+
const seq = ++loadSeqRef.current;
|
|
14
39
|
setLoading(true);
|
|
15
40
|
try {
|
|
16
41
|
const [inv, rows] = await Promise.all([fetchInvoice(id), fetchInvoiceItems(id)]);
|
|
42
|
+
if (seq !== loadSeqRef.current)
|
|
43
|
+
return;
|
|
44
|
+
const logo = await getInvoiceLogoUrl(inv?.branding_snapshot?.logo_path);
|
|
45
|
+
if (seq !== loadSeqRef.current)
|
|
46
|
+
return;
|
|
17
47
|
setInvoice(inv);
|
|
18
|
-
|
|
48
|
+
setDetails(detailsFromRecord(inv));
|
|
49
|
+
const safeRows = Array.isArray(rows) ? rows : [];
|
|
50
|
+
setLines(safeRows.map((r) => itemToEditLine(r, Number(inv?.vat_rate ?? DEFAULT_VAT_RATE))));
|
|
51
|
+
setLogoUrl(logo);
|
|
19
52
|
}
|
|
20
53
|
catch {
|
|
21
|
-
|
|
54
|
+
if (seq === loadSeqRef.current)
|
|
55
|
+
toast.error("Factuur kon niet worden geladen");
|
|
22
56
|
}
|
|
23
57
|
finally {
|
|
24
|
-
|
|
58
|
+
if (seq === loadSeqRef.current)
|
|
59
|
+
setLoading(false);
|
|
25
60
|
}
|
|
26
61
|
}, [id]);
|
|
27
62
|
useEffect(() => {
|
|
28
63
|
void load();
|
|
29
64
|
}, [load]);
|
|
65
|
+
const loadSiblings = useCallback(async () => {
|
|
66
|
+
setSiblingsLoading(true);
|
|
67
|
+
try {
|
|
68
|
+
const rows = await fetchInvoices();
|
|
69
|
+
setSiblingInvoices(Array.isArray(rows) ? rows : []);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
setSiblingInvoices(null);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setSiblingsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
void loadSiblings();
|
|
80
|
+
}, [loadSiblings]);
|
|
81
|
+
// Als de huidige factuur nog niet in de lijst staat (net aangemaakt), één keer opnieuw ophalen.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (siblingsLoading || !siblingInvoices || siblingRetryRef.current)
|
|
84
|
+
return;
|
|
85
|
+
if (siblingInvoices.some((i) => i.id === id))
|
|
86
|
+
return;
|
|
87
|
+
siblingRetryRef.current = true;
|
|
88
|
+
void loadSiblings();
|
|
89
|
+
}, [siblingInvoices, siblingsLoading, id, loadSiblings]);
|
|
90
|
+
const listIndex = siblingInvoices ? siblingInvoices.findIndex((i) => i.id === id) : -1;
|
|
91
|
+
// Lijst is nieuwste eerst: vorige op de lijst = rij erboven, volgende = rij eronder.
|
|
92
|
+
const prevInvoice = listIndex > 0 ? siblingInvoices[listIndex - 1] : undefined;
|
|
93
|
+
const nextInvoice = listIndex >= 0 && siblingInvoices && listIndex < siblingInvoices.length - 1
|
|
94
|
+
? siblingInvoices[listIndex + 1]
|
|
95
|
+
: undefined;
|
|
96
|
+
const goToSibling = (targetId) => {
|
|
97
|
+
if (!targetId)
|
|
98
|
+
return;
|
|
99
|
+
navigate({
|
|
100
|
+
to: "/facturen-installatietechniek/detail/$id",
|
|
101
|
+
params: { id: targetId },
|
|
102
|
+
search: { send: false },
|
|
103
|
+
});
|
|
104
|
+
};
|
|
30
105
|
const changeStatus = async (status) => {
|
|
31
106
|
setSaving(true);
|
|
32
107
|
try {
|
|
@@ -41,11 +116,147 @@ export function FactuurDetailPage() {
|
|
|
41
116
|
setSaving(false);
|
|
42
117
|
}
|
|
43
118
|
};
|
|
44
|
-
|
|
119
|
+
const save = async () => {
|
|
120
|
+
setSaving(true);
|
|
121
|
+
try {
|
|
122
|
+
await updateInvoiceFull(id, details, lines, invoice?.note ?? null);
|
|
123
|
+
toast.success("Factuur opgeslagen");
|
|
124
|
+
setEditing(false);
|
|
125
|
+
await load();
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
setSaving(false);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const pdfFilename = `Factuur-${invoice?.invoice_number ?? "concept"}.pdf`;
|
|
135
|
+
const openSend = async () => {
|
|
136
|
+
const num = invoice?.invoice_number ?? "";
|
|
137
|
+
const company = invoice?.branding_snapshot?.company_name ?? "";
|
|
138
|
+
let storedEmail = "";
|
|
139
|
+
try {
|
|
140
|
+
storedEmail = (await fetchCustomerEmail(invoice?.customer_id)) ?? "";
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
storedEmail = "";
|
|
144
|
+
}
|
|
145
|
+
setKnownEmail(storedEmail.trim());
|
|
146
|
+
const customerEmail = invoice?.sent_to || storedEmail;
|
|
147
|
+
setMail({
|
|
148
|
+
to: customerEmail,
|
|
149
|
+
subject: `Factuur ${num}${company ? ` van ${company}` : ""}`,
|
|
150
|
+
message: `Beste ${details.customer_attention || details.customer_name || "klant"},\n\nIn de bijlage vindt u factuur ${num}.\n\nMet vriendelijke groet,\n${company}`,
|
|
151
|
+
});
|
|
152
|
+
setSender(null);
|
|
153
|
+
setAttachPdf(true);
|
|
154
|
+
setPreparedPdf("");
|
|
155
|
+
setMailToTouched(false);
|
|
156
|
+
fetchInvoiceEmailPurpose().then(fetchSenderForPurpose).then(setSender).catch(() => setSender(null));
|
|
157
|
+
setSendOpen(true);
|
|
158
|
+
if (!sheetRef.current)
|
|
159
|
+
return;
|
|
160
|
+
setPreparingPdf(true);
|
|
161
|
+
try {
|
|
162
|
+
const pdf = await elementToPdfBase64(sheetRef.current);
|
|
163
|
+
setPreparedPdf(pdf);
|
|
164
|
+
downloadPdfBase64(pdf, pdfFilename);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
toast.error(error instanceof Error ? error.message : "De factuur-PDF kon niet worden aangemaakt");
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
setPreparingPdf(false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
autoOpenedRef.current = false;
|
|
175
|
+
}, [id]);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (loading || !invoice || !search.send || autoOpenedRef.current)
|
|
178
|
+
return;
|
|
179
|
+
autoOpenedRef.current = true;
|
|
180
|
+
void openSend();
|
|
181
|
+
void navigate({
|
|
182
|
+
to: "/facturen-installatietechniek/detail/$id",
|
|
183
|
+
params: { id },
|
|
184
|
+
search: { send: false },
|
|
185
|
+
replace: true,
|
|
186
|
+
});
|
|
187
|
+
}, [id, invoice, loading, navigate, search.send]);
|
|
188
|
+
const send = () => {
|
|
189
|
+
if (!isValidEmail(mail.to)) {
|
|
190
|
+
toast.error("Vul een geldig e-mailadres in");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!invoice)
|
|
194
|
+
return;
|
|
195
|
+
if (!attachPdf || !preparedPdf) {
|
|
196
|
+
toast.error("Vink de PDF-bijlage aan voordat u de factuur verzendt");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (mail.to.trim().toLowerCase() !== knownEmail.toLowerCase()) {
|
|
200
|
+
setConfirmOpen(true);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
void doSend();
|
|
204
|
+
};
|
|
205
|
+
const doSend = async () => {
|
|
206
|
+
setConfirmOpen(false);
|
|
207
|
+
if (!invoice || !isValidEmail(mail.to) || !attachPdf || !preparedPdf)
|
|
208
|
+
return;
|
|
209
|
+
const targetInvoice = nextInvoice ?? prevInvoice;
|
|
210
|
+
setSaving(true);
|
|
211
|
+
try {
|
|
212
|
+
const queued = await sendInvoiceEmail({
|
|
213
|
+
invoiceId: id,
|
|
214
|
+
to: mail.to.trim(),
|
|
215
|
+
subject: mail.subject,
|
|
216
|
+
message: mail.message,
|
|
217
|
+
pdfBase64: preparedPdf,
|
|
218
|
+
filename: pdfFilename,
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
await registerInvoiceSend(id, queued.outboxId);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
toast.warning(e instanceof Error ? e.message : "De e-mail staat in de wachtrij, maar de factuur kon niet als verzonden worden geregistreerd");
|
|
225
|
+
}
|
|
226
|
+
toast.success(`Factuur verzonden naar ${mail.to.trim()}`, {
|
|
227
|
+
description: "De factuur staat nu op Verzonden. De e-mailmodule verwerkt de verzending met de PDF als bijlage.",
|
|
228
|
+
});
|
|
229
|
+
setSendOpen(false);
|
|
230
|
+
if (targetInvoice) {
|
|
231
|
+
navigate({
|
|
232
|
+
to: "/facturen-installatietechniek/detail/$id",
|
|
233
|
+
params: { id: targetInvoice.id },
|
|
234
|
+
search: { send: false },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
navigate({ to: "/facturen-installatietechniek" });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
toast.error(e instanceof Error ? e.message : "Verzenden mislukt");
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
setSaving(false);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const changeSendOpen = (nextOpen) => {
|
|
249
|
+
setSendOpen(nextOpen);
|
|
250
|
+
if (!nextOpen) {
|
|
251
|
+
setPreparedPdf("");
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
if (loading && !invoice)
|
|
45
255
|
return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Laden\u2026" });
|
|
46
|
-
}
|
|
47
256
|
if (!invoice) {
|
|
48
257
|
return (_jsxs("main", { className: "space-y-4 p-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Factuur niet gevonden." }), _jsx(Button, { asChild: true, variant: "outline", children: _jsx(Link, { to: "/facturen-installatietechniek", children: "Terug naar overzicht" }) })] }));
|
|
49
258
|
}
|
|
50
|
-
|
|
259
|
+
const isConcept = invoice.status === "concept";
|
|
260
|
+
const wasSent = Boolean(invoice.sent_at);
|
|
261
|
+
return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "sticky top-16 z-20 -mx-6 flex flex-wrap items-center gap-3 border-b border-border/60 bg-background px-6 py-3", children: [_jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsxs(Link, { to: "/facturen-installatietechniek", className: "gap-1", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Overzicht"] }) }), _jsx("h1", { className: "text-xl font-semibold", children: invoice.invoice_number ?? "Concept" }), _jsxs(Badge, { variant: "outline", children: [SOURCE_LABELS[invoice.source_type] ?? invoice.source_type, invoice.source_number ? ` ${invoice.source_number}` : ""] }), invoice.sent_at ? (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Verzonden ", new Date(invoice.sent_at).toLocaleString("nl-NL"), " aan ", invoice.sent_to] })) : null, _jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-1 rounded-full border border-border bg-card p-1", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 rounded-full", "aria-label": "Vorige factuur op de lijst", title: "Vorige factuur op de lijst", disabled: editing || loading || siblingsLoading || !prevInvoice, onClick: () => goToSibling(prevInvoice?.id), children: _jsx(ChevronLeft, { className: "h-5 w-5" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 rounded-full", "aria-label": "Volgende factuur op de lijst", title: "Volgende factuur op de lijst", disabled: editing || loading || siblingsLoading || !nextInvoice, onClick: () => goToSibling(nextInvoice?.id), children: _jsx(ChevronRight, { className: "h-5 w-5" }) })] }), editing ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: () => { setEditing(false); void load(); }, disabled: saving, children: [_jsx(X, { className: "mr-2 h-4 w-4" }), "Annuleren"] }), _jsxs(Button, { onClick: save, disabled: saving, children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), saving ? "Opslaan…" : "Opslaan"] })] })) : (_jsxs(_Fragment, { children: [!wasSent ? (_jsxs(Button, { variant: "outline", onClick: () => (isConcept ? setEditing(true) : toast.error("Alleen conceptfacturen kunnen worden bewerkt. Zet de status terug op Concept.")), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Bewerken"] })) : null, _jsxs(Button, { onClick: () => void openSend(), children: [_jsx(Mail, { className: "mr-2 h-4 w-4" }), wasSent ? "Opnieuw verzenden" : "Verzenden"] })] })), _jsx("div", { className: "w-44", children: _jsxs(Select, { value: invoice.status, onValueChange: (v) => changeStatus(v), disabled: saving || editing, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.entries(STATUS_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] }) })] })] }), _jsx("div", { children: _jsx("div", { className: "min-w-0", children: _jsx(InvoiceA4Preview, { ref: sheetRef, branding: invoice.branding_snapshot ?? emptyInvoiceBranding(), details: details, lines: lines, invoiceNumber: invoice.invoice_number, logoUrl: logoUrl, printable: !editing, editable: editing, defaultVat: String(Number(invoice.vat_rate) || DEFAULT_VAT_RATE), onDetailsChange: setDetails, onLinesChange: setLines }) }) }), _jsx(Dialog, { open: sendOpen, onOpenChange: changeSendOpen, children: _jsxs(DialogContent, { className: "flex max-h-[92vh] max-w-[calc(100vw-2rem)] flex-col sm:max-w-lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Factuur verzenden" }) }), _jsxs("div", { className: "min-h-0 flex-1 space-y-3 overflow-y-auto pr-1", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "mail-to", children: "E-mailadres klant" }), _jsx(Input, { id: "mail-to", type: "email", value: mail.to, onChange: (e) => setMail({ ...mail, to: e.target.value }), onBlur: () => setMailToTouched(true), placeholder: "naam@bedrijf.nl", "aria-invalid": mailToTouched && !isValidEmail(mail.to) }), mailToTouched && !isValidEmail(mail.to) ? (_jsx("p", { className: "text-xs text-destructive", children: "Vul een geldig e-mailadres in, bijvoorbeeld naam@bedrijf.nl" })) : null] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "mail-subject", children: "Onderwerp" }), _jsx(Input, { id: "mail-subject", value: mail.subject, onChange: (e) => setMail({ ...mail, subject: e.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "mail-msg", children: "Bericht" }), _jsx(Textarea, { id: "mail-msg", rows: 6, value: mail.message, onChange: (e) => setMail({ ...mail, message: e.target.value }) })] }), _jsxs("label", { className: "flex cursor-pointer items-start gap-3 rounded-md border border-border p-3", children: [_jsx("input", { type: "checkbox", checked: attachPdf, onChange: (event) => setAttachPdf(event.target.checked), className: "mt-1 h-4 w-4 accent-primary" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium", children: [preparedPdf ? _jsx(FileCheck2, { className: "h-4 w-4 text-primary" }) : _jsx(Download, { className: "h-4 w-4" }), "Factuur als PDF meesturen"] }), _jsx("span", { className: "mt-1 block truncate text-xs text-muted-foreground", children: preparingPdf ? "PDF wordt aangemaakt en gedownload…" : preparedPdf ? `${pdfFilename} is gedownload en klaar als bijlage.` : "PDF kon niet worden voorbereid." })] })] }), !attachPdf ? _jsx("p", { className: "text-xs text-destructive", children: "Vink de PDF aan om deze e-mail te kunnen versturen." }) : null, _jsxs("p", { className: "text-xs text-muted-foreground", children: [sender ? _jsxs(_Fragment, { children: ["Verzonden vanaf ", _jsx("span", { className: "font-medium text-foreground", children: sender.account_email }), ". "] }) : _jsx("span", { className: "text-destructive", children: "Geen afzender ingesteld (Instellingen \u2192 Factuur e-mail). " }), "De factuur wordt als PDF-bijlage aangeboden aan de e-mailmodule."] }), _jsxs(Button, { className: "w-full", onClick: send, disabled: saving || preparingPdf || !attachPdf || !preparedPdf || !isValidEmail(mail.to), children: [_jsx(Mail, { className: "mr-2 h-4 w-4" }), saving ? "Bezig…" : preparingPdf ? "PDF voorbereiden…" : "Verzenden met PDF"] })] })] }) }), _jsx(Dialog, { open: confirmOpen, onOpenChange: setConfirmOpen, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Ander e-mailadres dan bekend" }) }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [knownEmail ? (_jsxs(_Fragment, { children: ["Dit adres wijkt af van het bekende adres van de klant (", _jsx("span", { className: "font-medium text-foreground", children: knownEmail }), ")."] })) : (_jsx(_Fragment, { children: "Er is geen e-mailadres van deze klant bekend." })), " ", "Weet je zeker dat je naar ", _jsx("span", { className: "font-medium text-foreground", children: mail.to.trim() }), " wilt verzenden?"] }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => setConfirmOpen(false), children: "Annuleren" }), _jsx(Button, { onClick: () => void doSend(), disabled: saving, children: "Toch verzenden" })] })] }) })] }));
|
|
51
262
|
}
|