@flowselections/facturen-installatie-techniek 1.0.3 → 1.4.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 (65) hide show
  1. package/dist-lib/components/ArticlePhoto.d.ts +8 -0
  2. package/dist-lib/components/ArticlePhoto.d.ts.map +1 -0
  3. package/dist-lib/components/ArticlePhoto.js +7 -0
  4. package/dist-lib/components/FacturenForInstallatieTechniek.d.ts.map +1 -1
  5. package/dist-lib/components/FacturenForInstallatieTechniek.js +155 -52
  6. package/dist-lib/components/FactuurDetailPage.d.ts.map +1 -1
  7. package/dist-lib/components/FactuurDetailPage.js +240 -13
  8. package/dist-lib/components/InvoiceA4Preview.d.ts +19 -0
  9. package/dist-lib/components/InvoiceA4Preview.d.ts.map +1 -0
  10. package/dist-lib/components/InvoiceA4Preview.js +159 -0
  11. package/dist-lib/components/InvoiceDetailsFields.d.ts +7 -0
  12. package/dist-lib/components/InvoiceDetailsFields.d.ts.map +1 -0
  13. package/dist-lib/components/InvoiceDetailsFields.js +21 -0
  14. package/dist-lib/components/ModuleErrorView.d.ts +6 -0
  15. package/dist-lib/components/ModuleErrorView.d.ts.map +1 -0
  16. package/dist-lib/components/ModuleErrorView.js +11 -0
  17. package/dist-lib/components/WerkbonPreviewPanel.d.ts +4 -0
  18. package/dist-lib/components/WerkbonPreviewPanel.d.ts.map +1 -0
  19. package/dist-lib/components/WerkbonPreviewPanel.js +40 -0
  20. package/dist-lib/components/settings/InstellingenPage.d.ts +5 -0
  21. package/dist-lib/components/settings/InstellingenPage.d.ts.map +1 -0
  22. package/dist-lib/components/settings/InstellingenPage.js +119 -0
  23. package/dist-lib/components/settings/InvoiceBrandingSettingsCard.d.ts +2 -0
  24. package/dist-lib/components/settings/InvoiceBrandingSettingsCard.d.ts.map +1 -0
  25. package/dist-lib/components/settings/InvoiceBrandingSettingsCard.js +64 -0
  26. package/dist-lib/components/settings/InvoiceEmailSettingsCard.d.ts +2 -0
  27. package/dist-lib/components/settings/InvoiceEmailSettingsCard.d.ts.map +1 -0
  28. package/dist-lib/components/settings/InvoiceEmailSettingsCard.js +41 -0
  29. package/dist-lib/components/settings/ModuleHealthSettingsCard.d.ts +3 -0
  30. package/dist-lib/components/settings/ModuleHealthSettingsCard.d.ts.map +1 -0
  31. package/dist-lib/components/settings/ModuleHealthSettingsCard.js +47 -0
  32. package/dist-lib/index.d.ts +11 -0
  33. package/dist-lib/index.d.ts.map +1 -1
  34. package/dist-lib/index.js +18 -2
  35. package/dist-lib/integrations/supabase/auth-middleware.d.ts +11 -22550
  36. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/auth-middleware.js +3 -1
  38. package/dist-lib/integrations/supabase/client.d.ts +3 -22548
  39. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  40. package/dist-lib/integrations/supabase/client.js +3 -11
  41. package/dist-lib/integrations/supabase/client.server.d.ts +2 -22547
  42. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  43. package/dist-lib/integrations/supabase/client.server.js +1 -0
  44. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
  45. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
  46. package/dist-lib/integrations/supabase/previewAuthStorage.js +99 -0
  47. package/dist-lib/integrations/supabase/types.d.ts +24886 -11380
  48. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  49. package/dist-lib/integrations/supabase/types.js +2 -0
  50. package/dist-lib/lib/articles.functions.d.ts +24 -0
  51. package/dist-lib/lib/articles.functions.d.ts.map +1 -0
  52. package/dist-lib/lib/articles.functions.js +112 -0
  53. package/dist-lib/lib/auth-attach.d.ts +6 -0
  54. package/dist-lib/lib/auth-attach.d.ts.map +1 -0
  55. package/dist-lib/lib/auth-attach.js +17 -0
  56. package/dist-lib/lib/config.d.ts +21 -0
  57. package/dist-lib/lib/config.d.ts.map +1 -0
  58. package/dist-lib/lib/config.js +22 -0
  59. package/dist-lib/lib/dependencies.d.ts +9 -0
  60. package/dist-lib/lib/dependencies.d.ts.map +1 -0
  61. package/dist-lib/lib/dependencies.js +43 -0
  62. package/dist-lib/lib/facturen.d.ts +200 -0
  63. package/dist-lib/lib/facturen.d.ts.map +1 -1
  64. package/dist-lib/lib/facturen.js +465 -16
  65. package/package.json +60 -8
@@ -0,0 +1,8 @@
1
+ type ArticlePhotoProps = {
2
+ src?: string | null;
3
+ alt: string;
4
+ className?: string;
5
+ };
6
+ export declare function ArticlePhoto({ src, alt, className }: ArticlePhotoProps): import("react").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=ArticlePhoto.d.ts.map
@@ -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":"AA2DA,wBAAgB,8BAA8B,gCAoZ7C"}
1
+ {"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AAiJA,wBAAgB,8BAA8B,gCAge7C"}
@@ -1,10 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { Link } from "@tanstack/react-router";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { WerkbonPreviewPanel } from "../components/WerkbonPreviewPanel";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { dependencyMessage } from "../lib/dependencies";
5
+ import { DEFAULT_VAT_RATE } from "../lib/config";
6
+ import { supabase } from "@flowselections/core";
7
+ import { Link, useNavigate } from "@tanstack/react-router";
4
8
  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, Trash2 } from "lucide-react";
6
- import { createInvoiceFromSource, euroToCents, fetchInvoices, fetchSourceLines, formatCents, listSources, SOURCE_LABELS, STATUS_LABELS, } from "../lib/facturen";
7
- const emptyLine = () => ({ description: "", quantity: "1", unit: "stuk", price: "" });
9
+ import { Plus } from "lucide-react";
10
+ import { createInvoiceFromSource, emptyInvoiceDetails, fetchInvoicesDetailed, fetchInvoiceKpis, fetchSourceLines, formatCents, listSources, SOURCE_LABELS, STATUS_LABELS, emptyInvoiceBranding, fetchInvoiceBranding, getInvoiceLogoUrl, editLineToBillable, emptyEditLine, peekNextInvoiceNumber, } from "../lib/facturen";
11
+ import { InvoiceA4Preview } from "./InvoiceA4Preview";
8
12
  function statusVariant(status) {
9
13
  if (status === "betaald")
10
14
  return "default";
@@ -14,35 +18,112 @@ function statusVariant(status) {
14
18
  return "outline";
15
19
  return "secondary";
16
20
  }
21
+ function InvoiceTable({ invoices, emptyText, onOpen, }) {
22
+ if (invoices.length === 0) {
23
+ return _jsx("p", { className: "text-sm text-muted-foreground", children: emptyText });
24
+ }
25
+ 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]
26
+ ? SOURCE_LABELS[invoice.source_type] + (invoice.source_number ? ` ${invoice.source_number}` : "")
27
+ : "—" }) }), _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))) })] }));
28
+ }
17
29
  export function FacturenForInstallatieTechniek() {
30
+ const navigate = useNavigate();
18
31
  const [invoices, setInvoices] = useState([]);
19
32
  const [loading, setLoading] = useState(true);
33
+ const [sessionExpired, setSessionExpired] = useState(false);
34
+ const [loadError, setLoadError] = useState(false);
35
+ const [hasMore, setHasMore] = useState(false);
36
+ const [loadingMore, setLoadingMore] = useState(false);
37
+ const [kpiData, setKpiData] = useState(null);
20
38
  const [statusFilter, setStatusFilter] = useState("alle");
21
39
  const [sourceFilter, setSourceFilter] = useState("alle");
40
+ const [paidPeriod, setPaidPeriod] = useState("maand");
22
41
  const [open, setOpen] = useState(false);
23
42
  const [saving, setSaving] = useState(false);
43
+ const createRef = useRef(false);
24
44
  // bron-tab
25
45
  const [sourceType, setSourceType] = useState("work_order");
26
46
  const [sources, setSources] = useState([]);
27
47
  const [sourcesLoading, setSourcesLoading] = useState(false);
28
48
  const [selectedSource, setSelectedSource] = useState("");
29
49
  // handmatig-tab
30
- const [customerName, setCustomerName] = useState("");
31
- const [invoiceDate, setInvoiceDate] = useState(() => new Date().toISOString().slice(0, 10));
32
- const [vatRate, setVatRate] = useState("21");
33
- const [lines, setLines] = useState([emptyLine()]);
50
+ const [pickedCustomerId, setPickedCustomerId] = useState(null);
51
+ const [tab, setTab] = useState("handmatig");
52
+ const [details, setDetails] = useState(emptyInvoiceDetails);
53
+ const [vatRate, setVatRate] = useState(String(DEFAULT_VAT_RATE));
54
+ const [lines, setLines] = useState([emptyEditLine()]);
55
+ const [nextNumber, setNextNumber] = useState(null);
56
+ const [linkedWorkOrder, setLinkedWorkOrder] = useState(null);
57
+ const [branding, setBranding] = useState(emptyInvoiceBranding);
58
+ const [logoUrl, setLogoUrl] = useState(null);
59
+ const resetForm = () => {
60
+ setPickedCustomerId(null);
61
+ setDetails(emptyInvoiceDetails(branding.payment_term_days));
62
+ setLines([emptyEditLine(vatRate)]);
63
+ setLinkedWorkOrder(null);
64
+ };
65
+ const openNew = () => {
66
+ resetForm();
67
+ setTab("handmatig");
68
+ setOpen(true);
69
+ };
70
+ useEffect(() => {
71
+ if (!open)
72
+ return;
73
+ fetchInvoiceBranding().then(async (row) => {
74
+ setBranding(row);
75
+ setLogoUrl(await getInvoiceLogoUrl(row.logo_path));
76
+ setDetails((current) => ({
77
+ ...current,
78
+ payment_term_days: current.payment_term_days === "30" ? String(row.payment_term_days) : current.payment_term_days,
79
+ }));
80
+ }).catch(() => toast.error("Factuurhuisstijl kon niet worden geladen"));
81
+ peekNextInvoiceNumber().then(setNextNumber);
82
+ }, [open]);
34
83
  const load = useCallback(async () => {
35
84
  setLoading(true);
36
85
  try {
37
- setInvoices(await fetchInvoices());
86
+ const { data: userData } = await supabase.auth.getUser();
87
+ if (!userData?.user) {
88
+ setSessionExpired(true);
89
+ setLoadError(false);
90
+ setInvoices([]);
91
+ return;
92
+ }
93
+ setSessionExpired(false);
94
+ const result = await fetchInvoicesDetailed({ status: statusFilter, source: sourceFilter });
95
+ setLoadError(result.failed);
96
+ setInvoices(result.failed ? [] : result.invoices);
97
+ setHasMore(result.hasMore);
98
+ fetchInvoiceKpis().then(setKpiData).catch(() => setKpiData(null));
38
99
  }
39
100
  catch {
40
- toast.error("Fout bij het laden van facturen");
101
+ setLoadError(true);
102
+ setInvoices([]);
41
103
  }
42
104
  finally {
43
105
  setLoading(false);
44
106
  }
45
- }, []);
107
+ }, [statusFilter, sourceFilter]);
108
+ const loadMore = useCallback(async () => {
109
+ setLoadingMore(true);
110
+ try {
111
+ const offset = invoices.filter((i) => i.status !== "concept").length;
112
+ const result = await fetchInvoicesDetailed({ offset, status: statusFilter, source: sourceFilter });
113
+ if (result.failed) {
114
+ toast.error("Meer facturen laden is mislukt");
115
+ return;
116
+ }
117
+ setInvoices((prev) => {
118
+ const seen = new Set(prev.map((i) => i.id));
119
+ return [...prev, ...result.invoices.filter((i) => !seen.has(i.id))];
120
+ });
121
+ setHasMore(result.hasMore);
122
+ }
123
+ finally {
124
+ setLoadingMore(false);
125
+ }
126
+ }, [invoices, statusFilter, sourceFilter]);
46
127
  useEffect(() => {
47
128
  void load();
48
129
  }, [load]);
@@ -57,7 +138,7 @@ export function FacturenForInstallatieTechniek() {
57
138
  if (active)
58
139
  setSources(rows);
59
140
  })
60
- .catch(() => toast.error("Bronnen konden niet worden geladen"))
141
+ .catch((e) => toast.error(dependencyMessage(e, sourceType === "work_order" ? "werkbonnen" : "offertes", "Bronnen")))
61
142
  .finally(() => {
62
143
  if (active)
63
144
  setSourcesLoading(false);
@@ -68,26 +149,43 @@ export function FacturenForInstallatieTechniek() {
68
149
  }, [open, sourceType]);
69
150
  const filtered = useMemo(() => invoices.filter((i) => (statusFilter === "alle" || i.status === statusFilter) &&
70
151
  (sourceFilter === "alle" || i.source_type === sourceFilter)), [invoices, statusFilter, sourceFilter]);
152
+ const conceptInvoices = useMemo(() => filtered.filter((i) => i.status === "concept"), [filtered]);
153
+ const sendOrPaidInvoices = useMemo(() => filtered.filter((i) => i.status !== "concept"), [filtered]);
154
+ const openInvoice = useCallback((id) => navigate({
155
+ to: "/facturen-installatietechniek/detail/$id",
156
+ params: { id },
157
+ search: { send: false },
158
+ }), [navigate]);
71
159
  const kpi = useMemo(() => {
72
160
  const now = new Date();
73
- const openCents = invoices
74
- .filter((i) => i.status === "verzonden" || i.status === "vervallen")
75
- .reduce((sum, i) => sum + (i.total_cents || 0), 0);
76
- const overdue = invoices.filter((i) => i.status === "vervallen").length;
77
- const monthCents = invoices
78
- .filter((i) => {
79
- const d = new Date(i.invoice_date);
80
- return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
81
- })
82
- .reduce((sum, i) => sum + (i.total_cents || 0), 0);
83
- return { openCents, overdue, monthCents };
84
- }, [invoices]);
161
+ const inPeriod = (d) => {
162
+ if (paidPeriod === "jaar")
163
+ return d.getFullYear() === now.getFullYear();
164
+ if (paidPeriod === "maand")
165
+ return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
166
+ // huidige kalenderweek (maandag t/m zondag)
167
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
168
+ start.setDate(start.getDate() - ((start.getDay() + 6) % 7));
169
+ const end = new Date(start);
170
+ end.setDate(end.getDate() + 7);
171
+ return d >= start && d < end;
172
+ };
173
+ const paidRows = Array.isArray(kpiData?.paidThisYear) ? kpiData.paidThisYear : [];
174
+ const paidMonthCents = paidRows
175
+ .filter((i) => inPeriod(new Date(i.invoice_date)))
176
+ .reduce((sum, i) => sum + (Number(i.total_cents) || 0), 0);
177
+ const openCount = kpiData?.openCount ?? 0;
178
+ return { paidMonthCents, openCount };
179
+ }, [kpiData, paidPeriod]);
85
180
  const createFromSource = async () => {
86
181
  const source = sources.find((s) => s.id === selectedSource);
87
182
  if (!source) {
88
183
  toast.error("Kies eerst een bron");
89
184
  return;
90
185
  }
186
+ if (createRef.current)
187
+ return;
188
+ createRef.current = true;
91
189
  setSaving(true);
92
190
  try {
93
191
  const sourceLines = await fetchSourceLines(sourceType, source.id);
@@ -111,51 +209,56 @@ export function FacturenForInstallatieTechniek() {
111
209
  toast.error("Factuur aanmaken mislukt");
112
210
  }
113
211
  finally {
212
+ createRef.current = false;
114
213
  setSaving(false);
115
214
  }
116
215
  };
117
216
  const createManual = async () => {
118
- const payloadLines = lines
119
- .filter((l) => l.description.trim() !== "")
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) {
217
+ const payloadLines = lines.filter((l) => l.description.trim() !== "").map(editLineToBillable);
218
+ if (!details.customer_name.trim() || payloadLines.length === 0) {
134
219
  toast.error("Vul een klantnaam en minimaal één regel in");
135
220
  return;
136
221
  }
222
+ if (createRef.current)
223
+ return;
224
+ createRef.current = true;
137
225
  setSaving(true);
138
226
  try {
139
227
  const result = await createInvoiceFromSource({
140
- sourceType: "manual",
141
- sourceId: null,
142
- customerName: customerName.trim(),
143
- vatRate: parseFloat(vatRate.replace(",", ".")) || 21,
144
- invoiceDate,
228
+ sourceType: linkedWorkOrder ? "work_order" : "manual",
229
+ sourceId: linkedWorkOrder?.id ?? null,
230
+ customerId: linkedWorkOrder?.customer_id ?? pickedCustomerId ?? null,
231
+ vatRate: parseFloat(vatRate.replace(",", ".")) || DEFAULT_VAT_RATE,
145
232
  lines: payloadLines,
233
+ details,
146
234
  });
147
- toast.success(`Factuur ${result.invoice_number} aangemaakt`);
148
- setCustomerName("");
149
- setLines([emptyLine()]);
235
+ toast.success(result.reused ? `Bestaand concept ${result.invoice_number}` : `Factuur ${result.invoice_number} aangemaakt`);
236
+ resetForm();
150
237
  setOpen(false);
151
- await load();
238
+ await navigate({
239
+ to: "/facturen-installatietechniek/detail/$id",
240
+ params: { id: result.id },
241
+ search: { send: true },
242
+ });
152
243
  }
153
244
  catch {
154
245
  toast.error("Factuur aanmaken mislukt");
155
246
  }
156
247
  finally {
248
+ createRef.current = false;
157
249
  setSaving(false);
158
250
  }
159
251
  };
160
- return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_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: formatCents(kpi.openCents) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Vervallen" }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: kpi.overdue })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Deze maand" }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: formatCents(kpi.monthCents) })] })] }), _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: () => setOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Nieuwe factuur"] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Facturen" }) }), _jsx(CardContent, { children: loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" })) : filtered.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen facturen gevonden." })) : (_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: filtered.map((invoice) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsx(Link, { to: "/facturen-installatietechniek/detail/$id", params: { id: invoice.id }, className: "underline-offset-2 hover:underline", children: invoice.invoice_number ?? "—" }) }), _jsx(TableCell, { children: invoice.customer_name ?? "—" }), _jsx(TableCell, { children: _jsxs(Badge, { variant: "outline", children: [SOURCE_LABELS[invoice.source_type] ?? invoice.source_type, invoice.source_number ? ` ${invoice.source_number}` : ""] }) }), _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))) })] })) })] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Nieuwe factuur" }) }), _jsxs(Tabs, { defaultValue: "bron", 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: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs("div", { className: "space-y-2 sm:col-span-1", children: [_jsx(Label, { htmlFor: "klant", children: "Klantnaam" }), _jsx(Input, { id: "klant", value: customerName, onChange: (e) => setCustomerName(e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "datum", children: "Factuurdatum" }), _jsx(Input, { id: "datum", type: "date", value: invoiceDate, onChange: (e) => setInvoiceDate(e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "btw", children: "Btw %" }), _jsx(Input, { id: "btw", value: vatRate, onChange: (e) => setVatRate(e.target.value) })] })] }), _jsxs("div", { className: "space-y-3", children: [_jsx(Label, { children: "Regels" }), lines.map((line, index) => (_jsxs("div", { className: "grid grid-cols-12 items-end gap-2", children: [_jsx(Input, { className: "col-span-5", placeholder: "Omschrijving", value: line.description, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, description: e.target.value } : l))) }), _jsx(Input, { className: "col-span-2", placeholder: "Aantal", value: line.quantity, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, quantity: e.target.value } : l))) }), _jsx(Input, { className: "col-span-2", placeholder: "Eenheid", value: line.unit, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, unit: e.target.value } : l))) }), _jsx(Input, { className: "col-span-2", placeholder: "Prijs", value: line.price, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, price: e.target.value } : l))) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "col-span-1", onClick: () => setLines((prev) => (prev.length === 1 ? [emptyLine()] : prev.filter((_, i) => i !== index))), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setLines((prev) => [...prev, emptyLine()]), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Regel toevoegen"] })] }), _jsx(Button, { onClick: createManual, disabled: saving, className: "w-full", children: saving ? "Bezig…" : "Factuur aanmaken" })] })] })] }) })] }));
252
+ 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) => {
253
+ setPickedCustomerId(c.id);
254
+ setDetails((d) => ({
255
+ ...d,
256
+ customer_name: c.name ?? "",
257
+ customer_address: c.billing_street ?? "",
258
+ customer_postal_code: c.billing_postcode ?? "",
259
+ customer_city: c.billing_city ?? "",
260
+ debtor_number: c.customer_number ?? "",
261
+ customer_vat_number: c.vat_number ?? "",
262
+ }));
263
+ } }), _jsx(Button, { onClick: createManual, disabled: saving, className: "w-full", children: saving ? "Bezig…" : "Factuur aanmaken" })] })] })] })] })] }) })] }));
161
264
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,iBAAiB,gCAsKhC"}
1
+ {"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,iBAAiB,gCA6bhC"}