@flowselections/facturen-installatie-techniek 1.4.5 → 1.5.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 (33) hide show
  1. package/README.md +27 -0
  2. package/dist-lib/components/FacturenForInstallatieTechniek.d.ts.map +1 -1
  3. package/dist-lib/components/FacturenForInstallatieTechniek.js +56 -25
  4. package/dist-lib/components/FactuurDetailPage.d.ts.map +1 -1
  5. package/dist-lib/components/FactuurDetailPage.js +26 -8
  6. package/dist-lib/components/InvoiceA4Preview.d.ts.map +1 -1
  7. package/dist-lib/components/InvoiceA4Preview.js +10 -6
  8. package/dist-lib/components/settings/InvoiceModuleSettingsCard.d.ts +2 -0
  9. package/dist-lib/components/settings/InvoiceModuleSettingsCard.d.ts.map +1 -0
  10. package/dist-lib/components/settings/InvoiceModuleSettingsCard.js +53 -0
  11. package/dist-lib/components/settings/ModuleHealthSettingsCard.d.ts.map +1 -1
  12. package/dist-lib/components/settings/ModuleHealthSettingsCard.js +2 -0
  13. package/dist-lib/index.d.ts +2 -0
  14. package/dist-lib/index.d.ts.map +1 -1
  15. package/dist-lib/index.js +5 -2
  16. package/dist-lib/integrations/supabase/types.d.ts +572 -1
  17. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  18. package/dist-lib/lib/config.d.ts +0 -4
  19. package/dist-lib/lib/config.d.ts.map +1 -1
  20. package/dist-lib/lib/config.js +2 -4
  21. package/dist-lib/lib/dependencies.d.ts +2 -0
  22. package/dist-lib/lib/dependencies.d.ts.map +1 -1
  23. package/dist-lib/lib/dependencies.js +19 -0
  24. package/dist-lib/lib/facturen.d.ts +45 -8
  25. package/dist-lib/lib/facturen.d.ts.map +1 -1
  26. package/dist-lib/lib/facturen.js +132 -48
  27. package/dist-lib/lib/rekenregels.d.ts +49 -0
  28. package/dist-lib/lib/rekenregels.d.ts.map +1 -0
  29. package/dist-lib/lib/rekenregels.js +135 -0
  30. package/dist-lib/lib/settings.d.ts +32 -0
  31. package/dist-lib/lib/settings.d.ts.map +1 -0
  32. package/dist-lib/lib/settings.js +117 -0
  33. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @flowselections/facturen-installatie-techniek
2
+
3
+ Facturen-module voor FlowSelections-shells: facturen maken uit werkbonnen/offertes of handmatig, bewerken, samenvoegen, als PDF versturen.
4
+
5
+ ## Instellingen (per database)
6
+ - **Btw-tarieven** — tabel `invoice_vat_rates` (tarief, omschrijving, standaard, actief). Standaard 21% (standaard) en 9%. "Btw verlegd" (0%) is altijd een aparte keuze. Beheer: Instellingen → Btw en tijdzone.
7
+ - **Tijdzone** — tabel `invoice_module_settings.timezone`, standaard `Europe/Amsterdam`. Bepaalt "vandaag" voor de factuurdatum en de week/maand/jaar-periodes.
8
+ - Ontbreken deze tabellen of rijen, dan gebruikt de module de standaardwaarden hierboven en logt een waarschuwing (`[facturen] instellingen ontbreken…`); de controlekaart toont dit ook.
9
+
10
+ ## Rekenregels
11
+ `src/lib/rekenregels.ts` bevat de gedeelde FlowSelections-rekenregels (bedragen naar centen, kalenderdatums, pakket-, reis-, afrondings- en mandaatregels). Deze module gebruikt `parseBedragNaarCenten`, `vandaagLokaal`, `plusDagen` en `lokaleDatum`; basispakketten rekenen nog met de bestaande factuurlogica.
12
+
13
+ ## Gedrag
14
+ - Bedragen en aantallen: "12,50", "12.50", "1.250,00" en "1,250.00" worden herkend; ongeldige invoer geeft "Ongeldig bedrag" en blokkeert opslaan.
15
+ - Per bron (werkbon/offerte) bestaat maximaal één niet-gecrediteerde factuur (unieke index + controle in `invoice_create_from_source`).
16
+ - Samenvoegen alleen bij dezelfde klant (klant-id) en hetzelfde btw-regime; ook in `invoice_merge` gecontroleerd.
17
+ - Opslaan controleert of een ander intussen wijzigde (`invoice_update_full_vat`, optionele `_expected_updated_at`).
18
+ - Lange facturen worden meerdere A4-pagina's met kopregel en paginanummer. De PDF wordt alleen gedownload via de knop "PDF downloaden".
19
+ - Verzenden naar een ander adres wijzigt het klantrecord niet.
20
+
21
+ ## Vereist in de omgeving
22
+ - Shell-sessie en Supabase-client via `@flowselections/core`.
23
+ - Opslagbuckets `layout-logos` en `artikel-fotos`.
24
+ - Server: `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY` (door de shell geleverd).
25
+
26
+ ## Tests
27
+ `bunx vitest@3 run`
@@ -1 +1 @@
1
- {"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AAmLA,wBAAgB,8BAA8B,gCAwiB7C"}
1
+ {"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AAqLA,wBAAgB,8BAA8B,gCAwjB7C"}
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { WerkbonPreviewPanel } from "./WerkbonPreviewPanel.js";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { dependencyMessage } from "../lib/dependencies";
5
- import { DEFAULT_VAT_RATE } from "../lib/config";
4
+ import { dependencyMessage, explainError } from "../lib/dependencies";
5
+ import { plusDagen, vandaagLokaal } from "../lib/rekenregels";
6
+ import { useInvoiceSettings } from "../lib/settings";
6
7
  import { supabase } from "@flowselections/core";
7
8
  import { Link, useNavigate } from "@tanstack/react-router";
8
9
  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";
9
10
  import { Plus } from "lucide-react";
10
- import { createInvoiceFromSource, emptyInvoiceDetails, fetchInvoicesDetailed, fetchInvoiceKpis, fetchSourceLines, formatCents, listSources, SOURCE_LABELS, STATUS_LABELS, emptyInvoiceBranding, fetchInvoiceBranding, getInvoiceLogoUrl, editLineToBillable, emptyEditLine, peekNextInvoiceNumber, canMerge, canSelectForMerge, mergeInvoices, } from "../lib/facturen";
11
+ import { createInvoiceFromSource, emptyInvoiceDetails, firstLineInputError, mergeBlockReason, fetchInvoicesDetailed, fetchInvoiceKpis, fetchSourceLines, formatCents, listSources, SOURCE_LABELS, STATUS_LABELS, emptyInvoiceBranding, fetchInvoiceBranding, getInvoiceLogoUrl, editLineToBillable, emptyEditLine, peekNextInvoiceNumber, canMerge, canSelectForMerge, mergeInvoices, } from "../lib/facturen";
11
12
  import { InvoiceA4Preview } from "./InvoiceA4Preview";
12
13
  function statusVariant(status) {
13
14
  if (status === "betaald")
@@ -34,7 +35,7 @@ function InvoiceTable({ invoices, emptyText, onOpen, selection, }) {
34
35
  onOpen(invoice.id);
35
36
  }, children: [selection && (_jsx(TableCell, { onClick: (e) => e.stopPropagation(), children: _jsx("input", { type: "checkbox", "aria-label": `Selecteer ${invoice.invoice_number ?? "factuur"}`, className: "h-4 w-4 accent-primary", checked: checked, disabled: !selectable, onChange: () => selection.onToggle(invoice) }) })), _jsx(TableCell, { className: "font-medium", children: selection ? (invoice.invoice_number ?? "—") : (_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]
36
37
  ? SOURCE_LABELS[invoice.source_type] + (invoice.source_number ? ` ${invoice.source_number}` : "")
37
- : "—" }) }), _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));
38
+ : "—" }) }), _jsx(TableCell, { children: invoice.invoice_date ? new Date(`${invoice.invoice_date}T12:00:00`).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));
38
39
  }) })] }));
39
40
  }
40
41
  export function FacturenForInstallatieTechniek() {
@@ -60,9 +61,18 @@ export function FacturenForInstallatieTechniek() {
60
61
  const mergeRef = useRef(false);
61
62
  const mergeIds = useMemo(() => new Set(mergeSelected.map((i) => i.id)), [mergeSelected]);
62
63
  const stopMerge = () => { setMergeMode(false); setMergeSelected([]); setMergeConfirm(false); };
63
- const toggleMerge = (invoice) => setMergeSelected((cur) => cur.some((i) => i.id === invoice.id)
64
- ? cur.filter((i) => i.id !== invoice.id)
65
- : canSelectForMerge(invoice, cur) ? [...cur, invoice] : cur);
64
+ const toggleMerge = (invoice) => {
65
+ if (!mergeSelected.some((i) => i.id === invoice.id)) {
66
+ const reason = mergeBlockReason(invoice, mergeSelected);
67
+ if (reason) {
68
+ toast.error(`Niet samen te voegen: ${reason}`);
69
+ return;
70
+ }
71
+ }
72
+ setMergeSelected((cur) => cur.some((i) => i.id === invoice.id)
73
+ ? cur.filter((i) => i.id !== invoice.id)
74
+ : canSelectForMerge(invoice, cur) ? [...cur, invoice] : cur);
75
+ };
66
76
  const doMerge = async () => {
67
77
  if (!canMerge(mergeSelected) || mergeRef.current)
68
78
  return;
@@ -75,7 +85,8 @@ export function FacturenForInstallatieTechniek() {
75
85
  await navigate({ to: "/facturen-installatietechniek/detail/$id", params: { id }, search: { send: true } });
76
86
  }
77
87
  catch (e) {
78
- toast.error(e instanceof Error ? e.message : "Samenvoegen mislukt");
88
+ console.error("Samenvoegen mislukt", e);
89
+ toast.error(`Samenvoegen mislukt: ${explainError(e)}`);
79
90
  }
80
91
  finally {
81
92
  mergeRef.current = false;
@@ -91,7 +102,9 @@ export function FacturenForInstallatieTechniek() {
91
102
  const [pickedCustomerId, setPickedCustomerId] = useState(null);
92
103
  const [tab, setTab] = useState("handmatig");
93
104
  const [details, setDetails] = useState(emptyInvoiceDetails);
94
- const [vatRate, setVatRate] = useState(String(DEFAULT_VAT_RATE));
105
+ const settings = useInvoiceSettings();
106
+ const [vatRate, setVatRate] = useState(String(settings.defaultVatRate));
107
+ useEffect(() => { setVatRate(String(settings.defaultVatRate)); }, [settings.defaultVatRate]);
95
108
  const [lines, setLines] = useState([emptyEditLine()]);
96
109
  const [nextNumber, setNextNumber] = useState(null);
97
110
  const [linkedWorkOrder, setLinkedWorkOrder] = useState(null);
@@ -198,26 +211,27 @@ export function FacturenForInstallatieTechniek() {
198
211
  search: { send: false },
199
212
  }), [navigate]);
200
213
  const kpi = useMemo(() => {
201
- const now = new Date();
214
+ // Kalenderdatums (yyyy-mm-dd) in de tijdzone uit de instellingen.
215
+ const today = vandaagLokaal(settings.timezone);
216
+ const weekday = (new Date(`${today}T00:00:00Z`).getUTCDay() + 6) % 7; // ma = 0
217
+ const weekStart = plusDagen(today, -weekday);
218
+ const weekEnd = plusDagen(weekStart, 7);
202
219
  const inPeriod = (d) => {
220
+ if (!d)
221
+ return false;
203
222
  if (paidPeriod === "jaar")
204
- return d.getFullYear() === now.getFullYear();
223
+ return d.slice(0, 4) === today.slice(0, 4);
205
224
  if (paidPeriod === "maand")
206
- return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
207
- // huidige kalenderweek (maandag t/m zondag)
208
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
209
- start.setDate(start.getDate() - ((start.getDay() + 6) % 7));
210
- const end = new Date(start);
211
- end.setDate(end.getDate() + 7);
212
- return d >= start && d < end;
225
+ return d.slice(0, 7) === today.slice(0, 7);
226
+ return d >= weekStart && d < weekEnd;
213
227
  };
214
228
  const paidRows = Array.isArray(kpiData?.paidThisYear) ? kpiData.paidThisYear : [];
215
229
  const paidMonthCents = paidRows
216
- .filter((i) => inPeriod(new Date(i.invoice_date)))
230
+ .filter((i) => inPeriod(String(i.invoice_date ?? "").slice(0, 10)))
217
231
  .reduce((sum, i) => sum + (Number(i.total_cents) || 0), 0);
218
232
  const openCount = kpiData?.openCount ?? 0;
219
233
  return { paidMonthCents, openCount };
220
- }, [kpiData, paidPeriod]);
234
+ }, [kpiData, paidPeriod, settings.timezone]);
221
235
  const createFromSource = async () => {
222
236
  const source = sources.find((s) => s.id === selectedSource);
223
237
  if (!source) {
@@ -229,6 +243,11 @@ export function FacturenForInstallatieTechniek() {
229
243
  createRef.current = true;
230
244
  setSaving(true);
231
245
  try {
246
+ const { data: sess } = await supabase.auth.getSession();
247
+ if (!sess?.session) {
248
+ toast.error("Factuur aanmaken mislukt: Je bent niet (meer) ingelogd. Log opnieuw in.");
249
+ return;
250
+ }
232
251
  const sourceLines = await fetchSourceLines(sourceType, source.id);
233
252
  if (sourceLines.length === 0) {
234
253
  toast.error("Deze bron heeft geen factureerbare regels");
@@ -246,8 +265,9 @@ export function FacturenForInstallatieTechniek() {
246
265
  setOpen(false);
247
266
  await load();
248
267
  }
249
- catch {
250
- toast.error("Factuur aanmaken mislukt");
268
+ catch (e) {
269
+ console.error("Factuur aanmaken mislukt", e);
270
+ toast.error(`Factuur aanmaken mislukt: ${explainError(e)}`);
251
271
  }
252
272
  finally {
253
273
  createRef.current = false;
@@ -255,6 +275,11 @@ export function FacturenForInstallatieTechniek() {
255
275
  }
256
276
  };
257
277
  const createManual = async () => {
278
+ const invalid = firstLineInputError(lines);
279
+ if (invalid) {
280
+ toast.error(invalid);
281
+ return;
282
+ }
258
283
  const payloadLines = lines.filter((l) => l.description.trim() !== "").map(editLineToBillable);
259
284
  if (!details.customer_name.trim() || payloadLines.length === 0) {
260
285
  toast.error("Vul een klantnaam en minimaal één regel in");
@@ -265,11 +290,16 @@ export function FacturenForInstallatieTechniek() {
265
290
  createRef.current = true;
266
291
  setSaving(true);
267
292
  try {
293
+ const { data: sess } = await supabase.auth.getSession();
294
+ if (!sess?.session) {
295
+ toast.error("Factuur aanmaken mislukt: Je bent niet (meer) ingelogd. Log opnieuw in.");
296
+ return;
297
+ }
268
298
  const result = await createInvoiceFromSource({
269
299
  sourceType: linkedWorkOrder ? "work_order" : "manual",
270
300
  sourceId: linkedWorkOrder?.id ?? null,
271
301
  customerId: linkedWorkOrder?.customer_id ?? pickedCustomerId ?? null,
272
- vatRate: parseFloat(vatRate.replace(",", ".")) || DEFAULT_VAT_RATE,
302
+ vatRate: Number(vatRate.replace(",", ".")) || settings.defaultVatRate,
273
303
  lines: payloadLines,
274
304
  details,
275
305
  });
@@ -282,8 +312,9 @@ export function FacturenForInstallatieTechniek() {
282
312
  search: { send: true },
283
313
  });
284
314
  }
285
- catch {
286
- toast.error("Factuur aanmaken mislukt");
315
+ catch (e) {
316
+ console.error("Factuur aanmaken mislukt", e);
317
+ toast.error(`Factuur aanmaken mislukt: ${explainError(e)}`);
287
318
  }
288
319
  finally {
289
320
  createRef.current = false;
@@ -1 +1 @@
1
- {"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AA8CA,wBAAgB,iBAAiB,gCA2chC"}
1
+ {"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AAgDA,wBAAgB,iBAAiB,gCAudhC"}
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
- import { DEFAULT_VAT_RATE } from "../lib/config";
3
+ import { useInvoiceSettings } from "../lib/settings";
4
4
  import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
5
5
  import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Label, Textarea, toast, } from "@flowselections/core";
6
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, setInvoiceNote, SOURCE_LABELS, STATUS_LABELS, updateInvoiceFull, } from "../lib/facturen";
7
+ import { detailsFromRecord, downloadPdfBase64, elementToPdfBase64, emptyInvoiceBranding, emptyInvoiceDetails, fetchCustomerEmail, fetchInvoice, fetchInvoiceEmailPurpose, fetchSenderForPurpose, fetchInvoiceItems, fetchInvoices, getInvoiceLogoUrl, itemToEditLine, registerInvoiceSend, sendInvoiceEmail, setInvoiceNote, SOURCE_LABELS, STATUS_LABELS, updateInvoiceFull, firstLineInputError, InvoiceConflictError, } from "../lib/facturen";
8
8
  import { InvoiceA4Preview } from "./InvoiceA4Preview";
9
9
  const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
10
10
  const isValidEmail = (value) => EMAIL_PATTERN.test(value.trim());
@@ -39,6 +39,9 @@ export function FactuurDetailPage() {
39
39
  const [reverseCharge, setReverseCharge] = useState(false);
40
40
  const [noteSaving, setNoteSaving] = useState(false);
41
41
  const [noteSavedAt, setNoteSavedAt] = useState(null);
42
+ const settings = useInvoiceSettings();
43
+ const settingsRef = useRef(settings);
44
+ settingsRef.current = settings;
42
45
  const loadSeqRef = useRef(0);
43
46
  const load = useCallback(async () => {
44
47
  const seq = ++loadSeqRef.current;
@@ -56,7 +59,7 @@ export function FactuurDetailPage() {
56
59
  setNoteSavedAt(null);
57
60
  setDetails(detailsFromRecord(inv));
58
61
  const safeRows = Array.isArray(rows) ? rows : [];
59
- setLines(safeRows.map((r) => itemToEditLine(r, Number(inv?.vat_rate ?? DEFAULT_VAT_RATE))));
62
+ setLines(safeRows.map((r) => itemToEditLine(r, Number(inv?.vat_rate ?? settingsRef.current.defaultVatRate))));
60
63
  setLogoUrl(logo);
61
64
  }
62
65
  catch {
@@ -111,18 +114,29 @@ export function FactuurDetailPage() {
111
114
  search: { send: false },
112
115
  });
113
116
  };
117
+ const saveRef = useRef(false);
118
+ const lineError = firstLineInputError(lines);
114
119
  const save = async () => {
120
+ if (saveRef.current)
121
+ return;
122
+ if (lineError) {
123
+ toast.error(`Kan niet opslaan: ${lineError}`);
124
+ return;
125
+ }
126
+ saveRef.current = true;
115
127
  setSaving(true);
116
128
  try {
117
- await updateInvoiceFull(id, details, lines, invoice?.note ?? null, reverseCharge);
129
+ await updateInvoiceFull(id, details, lines, invoice?.note ?? null, reverseCharge, invoice?.updated_at ?? null);
118
130
  toast.success("Factuur opgeslagen");
119
131
  setEditing(false);
120
132
  await load();
121
133
  }
122
134
  catch (e) {
123
- toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
135
+ console.error("[facturen] opslaan mislukt", e);
136
+ toast.error(e instanceof InvoiceConflictError ? e.message : e instanceof Error ? e.message : "Opslaan mislukt");
124
137
  }
125
138
  finally {
139
+ saveRef.current = false;
126
140
  setSaving(false);
127
141
  }
128
142
  };
@@ -174,9 +188,8 @@ export function FactuurDetailPage() {
174
188
  return;
175
189
  setPreparingPdf(true);
176
190
  try {
177
- const pdf = await elementToPdfBase64(sheetRef.current);
191
+ const pdf = await elementToPdfBase64(sheetRef.current, { header: `Factuur ${invoice?.invoice_number ?? ""}`.trim() });
178
192
  setPreparedPdf(pdf);
179
- downloadPdfBase64(pdf, pdfFilename);
180
193
  }
181
194
  catch (error) {
182
195
  toast.error(error instanceof Error ? error.message : "De factuur-PDF kon niet worden aangemaakt");
@@ -281,5 +294,10 @@ export function FactuurDetailPage() {
281
294
  }
282
295
  const isConcept = invoice.status === "concept";
283
296
  const wasSent = Boolean(invoice.sent_at);
284
- 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(Badge, { variant: "secondary", className: "px-3 py-1.5 text-sm", title: "De status wordt automatisch bepaald", children: STATUS_LABELS[invoice.status] ?? invoice.status })] })] }), _jsxs("div", { className: "flex flex-col gap-6 xl:flex-row xl:items-start", children: [_jsxs("aside", { className: "w-full shrink-0 space-y-2 rounded-lg border border-border bg-card p-4 xl:sticky xl:top-36 xl:w-72", children: [_jsx(Label, { htmlFor: "inv-note", className: "text-sm font-semibold", children: "Notitie" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Alleen intern. Komt niet op de factuur of in de e-mail." }), _jsx(Textarea, { id: "inv-note", rows: 6, value: noteText, onChange: (e) => setNoteText(e.target.value), placeholder: "Typ hier een notitie\u2026" }), _jsxs(Button, { size: "sm", className: "w-full", onClick: () => void saveNote(), disabled: noteSaving || noteText === (invoice.note ?? ""), children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), noteSaving ? "Opslaan…" : "Notitie opslaan"] }), noteSavedAt ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Laatst opgeslagen ", new Date(noteSavedAt).toLocaleString("nl-NL")] })) : null] }), _jsx("div", { className: "min-w-0 flex-1", 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, reverseCharge: reverseCharge, onReverseChargeChange: setReverseCharge }) })] }), _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: [!senderChecked ? _jsx(_Fragment, { children: "Afzender controleren\u2026 " }) : 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 || (senderChecked && !sender) || !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" })] })] }) })] }));
297
+ 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 || Boolean(lineError), title: lineError ?? undefined, 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(Badge, { variant: "secondary", className: "px-3 py-1.5 text-sm", title: "De status wordt automatisch bepaald", children: STATUS_LABELS[invoice.status] ?? invoice.status })] })] }), _jsxs("div", { className: "flex flex-col gap-6 xl:flex-row xl:items-start", children: [_jsxs("aside", { className: "w-full shrink-0 space-y-2 rounded-lg border border-border bg-card p-4 xl:sticky xl:top-36 xl:w-72", children: [_jsx(Label, { htmlFor: "inv-note", className: "text-sm font-semibold", children: "Notitie" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Alleen intern. Komt niet op de factuur of in de e-mail." }), _jsx(Textarea, { id: "inv-note", rows: 6, value: noteText, onChange: (e) => setNoteText(e.target.value), placeholder: "Typ hier een notitie\u2026" }), _jsxs(Button, { size: "sm", className: "w-full", onClick: () => void saveNote(), disabled: noteSaving || noteText === (invoice.note ?? ""), children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), noteSaving ? "Opslaan…" : "Notitie opslaan"] }), noteSavedAt ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Laatst opgeslagen ", new Date(noteSavedAt).toLocaleString("nl-NL")] })) : null] }), _jsx("div", { className: "min-w-0 flex-1", 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) || settings.defaultVatRate), onDetailsChange: setDetails, onLinesChange: setLines, reverseCharge: reverseCharge, onReverseChargeChange: setReverseCharge }) })] }), _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…" : preparedPdf ? `${pdfFilename} is klaar als bijlage.` : "PDF kon niet worden voorbereid." })] })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: !preparedPdf || preparingPdf, onClick: () => { try {
298
+ downloadPdfBase64(preparedPdf, pdfFilename);
299
+ }
300
+ catch (e) {
301
+ toast.error(e instanceof Error ? e.message : "Downloaden mislukt");
302
+ } }, children: [_jsx(Download, { className: "mr-2 h-4 w-4" }), "PDF downloaden"] }), !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: [!senderChecked ? _jsx(_Fragment, { children: "Afzender controleren\u2026 " }) : 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 || (senderChecked && !sender) || !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" })] })] }) })] }));
285
303
  }
@@ -1 +1 @@
1
- {"version":3,"file":"InvoiceA4Preview.d.ts","sourceRoot":"","sources":["../../src/components/InvoiceA4Preview.tsx"],"names":[],"mappings":"AAMA,OAAO,EAUL,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EAGpB,MAAM,iBAAiB,CAAC;AAIzB,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9C,CAAC;AAqDF,eAAO,MAAM,gBAAgB,+FAmN3B,CAAC"}
1
+ {"version":3,"file":"InvoiceA4Preview.d.ts","sourceRoot":"","sources":["../../src/components/InvoiceA4Preview.tsx"],"names":[],"mappings":"AAOA,OAAO,EAWL,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EAGpB,MAAM,iBAAiB,CAAC;AAIzB,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9C,CAAC;AAoDF,eAAO,MAAM,gBAAgB,+FAuN3B,CAAC"}
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useMemo, useState } from "react";
3
- import { DEFAULT_VAT_RATE, SEARCH_DEBOUNCE_MS, VAT_OPTIONS as CONFIG_VAT_OPTIONS } from "../lib/config";
3
+ import { SEARCH_DEBOUNCE_MS } from "../lib/config";
4
+ import { useInvoiceSettings } from "../lib/settings";
4
5
  import { dependencyMessage } from "../lib/dependencies";
5
6
  import { useServerFn } from "@tanstack/react-start";
6
7
  import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "@flowselections/core";
7
8
  import { ChevronDown, Minus, Package, Plus, Printer, Search, Trash2 } from "lucide-react";
8
- import { applyVatChoice, editLineToBillable, emptyEditLine, searchCustomers, fetchPackages, formatCents, basisLinePatch, isBasisPackage, splitQuarterDescription, } from "../lib/facturen";
9
+ import { applyVatChoice, editLineToBillable, lineInputErrors, emptyEditLine, searchCustomers, fetchPackages, formatCents, basisLinePatch, isBasisPackage, splitQuarterDescription, } from "../lib/facturen";
9
10
  import { listInvoiceArticles } from "../lib/articles.functions";
10
11
  import { ArticlePhoto } from "./ArticlePhoto";
11
- const VAT_OPTIONS = CONFIG_VAT_OPTIONS;
12
12
  function CustomerPicker({ details, onDetailsChange, onPick }) {
13
13
  const [open, setOpen] = useState(false);
14
14
  const [q, setQ] = useState("");
@@ -38,8 +38,11 @@ function CustomerPicker({ details, onDetailsChange, onPick }) {
38
38
  const filtered = Array.isArray(items) ? items : [];
39
39
  return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { value: details.customer_name, placeholder: "Klantnaam", onChange: (e) => onDetailsChange?.({ ...details, customer_name: e.target.value }), className: "w-full rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 font-semibold outline-none focus:border-primary" }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "h-6 shrink-0 px-1.5 print:hidden", "aria-label": "Kies klant", children: _jsx(ChevronDown, { className: "h-3.5 w-3.5" }) }) }), _jsxs(PopoverContent, { align: "start", className: "w-80 p-2", children: [_jsx(Input, { autoFocus: true, placeholder: "Zoek klant\u2026", value: q, onChange: (e) => setQ(e.target.value) }), _jsx("div", { className: "mt-2 max-h-64 overflow-y-auto", children: error ? _jsx("p", { className: "p-2 text-xs text-destructive", children: error }) : loading ? _jsx("p", { className: "p-2 text-xs text-muted-foreground", children: "Laden\u2026" }) : filtered.length === 0 ? _jsx("p", { className: "p-2 text-xs text-muted-foreground", children: "Geen klanten gevonden." }) : filtered.map((c) => (_jsxs("button", { type: "button", onClick: () => { onPick(c); setOpen(false); setQ(""); }, className: "block w-full rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent", children: [_jsx("div", { className: "font-medium", children: c.name ?? "—" }), _jsx("div", { className: "text-xs text-muted-foreground", children: [c.customer_number, c.billing_city].filter(Boolean).join(" · ") })] }, c.id))) })] })] })] }));
40
40
  }
41
- export const InvoiceA4Preview = forwardRef(function InvoiceA4Preview({ branding, details, lines, invoiceNumber, numberIsProvisional, logoUrl, printable = true, editable = false, defaultVat = String(DEFAULT_VAT_RATE), onDetailsChange, onLinesChange, onCustomerPick, reverseCharge = false, onReverseChargeChange }, ref) {
41
+ export const InvoiceA4Preview = forwardRef(function InvoiceA4Preview({ branding, details, lines, invoiceNumber, numberIsProvisional, logoUrl, printable = true, editable = false, defaultVat: defaultVatProp, onDetailsChange, onLinesChange, onCustomerPick, reverseCharge = false, onReverseChargeChange }, ref) {
42
42
  const safeLines = Array.isArray(lines) ? lines : [];
43
+ const settings = useInvoiceSettings();
44
+ const defaultVat = defaultVatProp ?? String(settings.defaultVatRate);
45
+ const VAT_OPTIONS = settings.vatRates.filter((r) => r.active).map((r) => String(r.rate));
43
46
  const listArticles = useServerFn(listInvoiceArticles);
44
47
  const [articles, setArticles] = useState([]);
45
48
  const articleIds = safeLines.map((line) => line.articleId).filter((id) => Boolean(id));
@@ -94,8 +97,9 @@ export const InvoiceA4Preview = forwardRef(function InvoiceA4Preview({ branding,
94
97
  const fmtDate = (v) => (v ? new Date(`${v}T12:00:00`).toLocaleDateString("nl-NL") : "—");
95
98
  return (_jsxs("div", { className: "space-y-2", children: [printable ? (_jsx("div", { className: "flex justify-end print:hidden", children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => window.print(), children: [_jsx(Printer, { className: "mr-2 h-4 w-4" }), "Afdrukken"] }) })) : null, _jsxs("article", { ref: ref, className: "invoice-print-target mx-auto flex min-h-[1123px] w-full max-w-[794px] flex-col bg-card text-card-foreground shadow-sm ring-1 ring-border", style: { "--invoice-primary": branding.primary_color, "--invoice-secondary": branding.secondary_color }, children: [_jsxs("header", { className: "flex h-36 items-stretch bg-[var(--invoice-primary)] px-8 py-4 text-primary-foreground", children: [_jsxs("div", { className: "flex flex-1 items-center gap-3", children: [_jsx("div", { className: "flex h-full items-center", children: logoUrl ? _jsx("img", { src: logoUrl, alt: `${branding.company_name} logo`, crossOrigin: "anonymous", className: "max-h-full max-w-56 object-contain" }) : _jsx("span", { className: "text-xs opacity-75", children: "BEDRIJFSLOGO" }) }), branding.show_company_name !== false ? _jsx("div", { className: "text-2xl font-semibold", children: branding.company_name || "Bedrijfsnaam" }) : null] }), _jsx("div", { className: "flex flex-col items-start justify-center text-left text-[11px] leading-4", children: contact.map((v) => _jsx("div", { children: v }, v)) })] }), _jsxs("div", { className: "flex flex-1 flex-col px-10 py-8 text-xs", children: [_jsxs("div", { className: "mb-8 flex items-start justify-between gap-8", children: [_jsxs("div", { className: "w-64 space-y-0.5 leading-5", children: [editable && onCustomerPick ? (_jsx(CustomerPicker, { details: details, onDetailsChange: onDetailsChange, onPick: onCustomerPick })) : (F({ k: "customer_name", placeholder: "Klantnaam", className: "font-semibold" })), editable || details.customer_attention ? _jsx("div", { children: F({ k: "customer_attention", placeholder: "T.a.v." }) }) : null, _jsx("div", { children: F({ k: "customer_address", placeholder: "Adres" }) }), _jsxs("div", { className: "flex gap-1", children: [F({ k: "customer_postal_code", placeholder: "Postcode", className: "w-24" }), F({ k: "customer_city", placeholder: "Plaats" })] }), editable || details.customer_vat_number ? _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "shrink-0", children: "Btw-nr:" }), F({ k: "customer_vat_number", placeholder: "—" })] }) : null] }), _jsxs("div", { className: "min-w-72", children: [_jsx("h2", { className: "mb-3 text-2xl font-semibold", children: "FACTUUR" }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] items-center gap-x-5 gap-y-1", children: [_jsx("dt", { children: "Factuurnummer" }), _jsxs("dd", { className: "font-medium", children: [number, numberIsProvisional && invoiceNumber ? _jsx("span", { className: "ml-1 text-[10px] font-normal text-muted-foreground print:hidden", children: "(voorlopig)" }) : null] }), _jsx("dt", { children: "Factuurdatum" }), _jsx("dd", { children: editable ? F({ k: "invoice_date", placeholder: "", type: "date" }) : fmtDate(details.invoice_date) }), _jsx("dt", { children: "Betaaltermijn" }), _jsx("dd", { children: editable ? _jsxs("span", { className: "flex items-center gap-1", children: [F({ k: "payment_term_days", placeholder: "30", className: "w-14" }), "dagen"] }) : `${details.payment_term_days || branding.payment_term_days} dagen` }), _jsx("dt", { children: "Vervaldatum" }), _jsx("dd", { children: formatDueDate(details.invoice_date, details.payment_term_days) }), _jsx("dt", { children: "Debiteurnummer" }), _jsx("dd", { children: F({ k: "debtor_number", placeholder: "—" }) }), _jsx("dt", { children: "Referentie" }), _jsx("dd", { children: F({ k: "reference", placeholder: "—" }) }), _jsx("dt", { children: "Werkbonnummer" }), _jsx("dd", { children: F({ k: "source_number", placeholder: "—" }) }), _jsx("dt", { children: "Werkadres" }), _jsx("dd", { children: F({ k: "work_address", placeholder: "—" }) }), _jsx("dt", { children: "Uitvoerdatum" }), _jsx("dd", { children: editable ? F({ k: "execution_date", placeholder: "", type: "date" }) : fmtDate(details.execution_date) })] })] })] }), _jsxs("table", { className: "w-full table-fixed border-collapse text-left", children: [_jsx("thead", { className: "border-y border-foreground/30", children: _jsxs("tr", { children: [_jsx("th", { className: "w-[44%] py-2", children: "Omschrijving" }), _jsx("th", { className: "w-[12%] py-2 text-right", children: "Aantal" }), _jsx("th", { className: "w-[15%] py-2 text-right", children: "Prijs" }), _jsx("th", { className: "w-[11%] py-2 text-right", children: "Btw" }), _jsx("th", { className: "w-[15%] py-2 text-right", children: "Totaal" }), editable ? _jsx("th", { className: "w-[3%]" }) : null] }) }), _jsx("tbody", { children: safeLines.length ? safeLines.map((line, i) => {
96
99
  const c = computed[i];
97
- return (_jsx("tr", { className: "border-b border-border/60 align-top", children: editable ? (_jsxs(_Fragment, { children: [_jsx("td", { className: "py-1 pr-2", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("textarea", { rows: 1, value: line.description, placeholder: "Omschrijving", readOnly: Boolean(line.articleId) || isBasisLine(line), onChange: (e) => setL(i, { description: e.target.value }), className: "min-w-0 flex-1 resize-y rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 outline-none focus:border-primary read-only:border-transparent read-only:bg-muted/40" }), isBasisLine(line) ? _jsx(BasisBadge, {}) : null, line.articleId ? _jsx(ArticlePhoto, { src: articleById.get(line.articleId)?.photo_url ?? line.photoUrl, alt: line.description, className: "h-9 w-9" }) : null] }) }), _jsx("td", { className: "py-1", children: isBasisLine(line) ? (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx("button", { type: "button", "aria-label": "Kwartier minder", onClick: () => stepQuarter(i, -1), disabled: (line.extraQuarters ?? splitQuarterDescription(line.description).quarters) <= 0, className: "rounded border border-border p-0.5 disabled:opacity-40", children: _jsx(Minus, { className: "h-3 w-3" }) }), _jsxs("span", { className: "whitespace-nowrap text-xs tabular-nums", children: ["+", line.extraQuarters ?? splitQuarterDescription(line.description).quarters, " kw"] }), _jsx("button", { type: "button", "aria-label": "Kwartier meer", onClick: () => stepQuarter(i, 1), className: "rounded border border-border p-0.5", children: _jsx(Plus, { className: "h-3 w-3" }) })] })) : _jsx("input", { value: line.quantity, onChange: (e) => setL(i, { quantity: e.target.value }), className: "w-full rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 text-right outline-none focus:border-primary" }) }), _jsx("td", { className: "py-1 pl-1", children: _jsx("input", { value: line.price, placeholder: "0,00", readOnly: isBasisLine(line), onChange: (e) => setL(i, { price: e.target.value }), className: "w-full rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 text-right outline-none focus:border-primary read-only:border-transparent read-only:bg-muted/40" }) }), _jsx("td", { className: "py-1 pl-1", children: _jsx("select", { value: line.vat, disabled: Boolean(line.articleId) || reverseCharge, onChange: (e) => setL(i, { vat: e.target.value }), className: "w-full rounded-sm border border-dashed border-border bg-transparent px-0.5 py-0.5 text-right outline-none disabled:border-transparent disabled:bg-muted/40 disabled:opacity-100", children: [...new Set([...VAT_OPTIONS, line.vat])].map((v) => _jsxs("option", { value: v, children: [v, "%"] }, v)) }) }), _jsx("td", { className: "py-1.5 text-right", children: formatCents(c.line_total_cents) }), _jsx("td", { className: "py-1 text-right print:hidden", children: _jsx("button", { type: "button", "aria-label": "Regel verwijderen", onClick: () => onLinesChange?.(safeLines.filter((_, idx) => idx !== i)), className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) })] })) : (_jsxs(_Fragment, { children: [_jsx("td", { className: "whitespace-pre-wrap break-words py-2 pr-3", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "min-w-0 flex-1", children: line.description || "—" }), isBasisLine(line) ? _jsx(BasisBadge, {}) : null, line.articleId ? _jsx(ArticlePhoto, { src: articleById.get(line.articleId)?.photo_url ?? line.photoUrl, alt: line.description, className: "h-9 w-9" }) : null] }) }), _jsxs("td", { className: "py-2 text-right", children: [c.quantity, " ", line.unit] }), _jsx("td", { className: "py-2 text-right", children: formatCents(c.unit_price_cents) }), _jsxs("td", { className: "py-2 text-right", children: [c.vat_rate, "%"] }), _jsx("td", { className: "py-2 text-right", children: formatCents(c.line_total_cents) })] })) }, i));
98
- }) : _jsx("tr", { children: _jsx("td", { colSpan: editable ? 6 : 5, className: "py-8 text-center text-muted-foreground", children: "Nog geen factuurregels" }) }) })] }), editable ? _jsx(AddLineBar, { defaultVat: reverseCharge ? "0" : defaultVat, onAdd: (l) => onLinesChange?.([...safeLines, l]) }) : null, _jsxs("div", { className: "mt-6 ml-auto w-64 space-y-1.5", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Totaal excl. btw" }), _jsx("span", { children: formatCents(subtotal) })] }), editable && onReverseChargeChange ? (_jsxs("div", { className: "flex items-center justify-between gap-2", "data-html2canvas-ignore": true, children: [_jsx("span", { children: "Btw factuur" }), _jsx("div", { className: "flex overflow-hidden rounded-md border border-border", children: ["21", "9", "verlegd"].map((c) => {
100
+ const errs = editable ? lineInputErrors(line) : {};
101
+ return (_jsx("tr", { className: "border-b border-border/60 align-top", children: editable ? (_jsxs(_Fragment, { children: [_jsx("td", { className: "py-1 pr-2", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("textarea", { rows: 1, value: line.description, placeholder: "Omschrijving", readOnly: Boolean(line.articleId) || isBasisLine(line), onChange: (e) => setL(i, { description: e.target.value }), className: "min-w-0 flex-1 resize-y rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 outline-none focus:border-primary read-only:border-transparent read-only:bg-muted/40" }), isBasisLine(line) ? _jsx(BasisBadge, {}) : null, line.articleId ? _jsx(ArticlePhoto, { src: articleById.get(line.articleId)?.photo_url ?? line.photoUrl, alt: line.description, className: "h-9 w-9" }) : null] }) }), _jsx("td", { className: "py-1", children: isBasisLine(line) ? (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx("button", { type: "button", "aria-label": "Kwartier minder", onClick: () => stepQuarter(i, -1), disabled: (line.extraQuarters ?? splitQuarterDescription(line.description).quarters) <= 0, className: "rounded border border-border p-0.5 disabled:opacity-40", children: _jsx(Minus, { className: "h-3 w-3" }) }), _jsxs("span", { className: "whitespace-nowrap text-xs tabular-nums", children: ["+", line.extraQuarters ?? splitQuarterDescription(line.description).quarters, " kw"] }), _jsx("button", { type: "button", "aria-label": "Kwartier meer", onClick: () => stepQuarter(i, 1), className: "rounded border border-border p-0.5", children: _jsx(Plus, { className: "h-3 w-3" }) })] })) : _jsxs(_Fragment, { children: [_jsx("input", { value: line.quantity, "aria-invalid": Boolean(errs.quantity), onChange: (e) => setL(i, { quantity: e.target.value }), className: `w-full rounded-sm border border-dashed bg-transparent px-1 py-0.5 text-right outline-none focus:border-primary ${errs.quantity ? "border-destructive" : "border-border"}` }), errs.quantity ? _jsx("span", { className: "block text-right text-[10px] text-destructive", "data-html2canvas-ignore": true, children: errs.quantity }) : null] }) }), _jsxs("td", { className: "py-1 pl-1", children: [_jsx("input", { value: line.price, placeholder: "0,00", "aria-invalid": Boolean(errs.price), readOnly: isBasisLine(line), onChange: (e) => setL(i, { price: e.target.value }), className: `w-full rounded-sm border border-dashed bg-transparent px-1 py-0.5 text-right outline-none focus:border-primary read-only:border-transparent read-only:bg-muted/40 ${errs.price ? "border-destructive" : "border-border"}` }), errs.price ? _jsx("span", { className: "block text-right text-[10px] text-destructive", "data-html2canvas-ignore": true, children: errs.price }) : null] }), _jsx("td", { className: "py-1 pl-1", children: _jsx("select", { value: line.vat, disabled: Boolean(line.articleId) || reverseCharge, onChange: (e) => setL(i, { vat: e.target.value }), className: "w-full rounded-sm border border-dashed border-border bg-transparent px-0.5 py-0.5 text-right outline-none disabled:border-transparent disabled:bg-muted/40 disabled:opacity-100", children: [...new Set([...VAT_OPTIONS, line.vat])].map((v) => _jsxs("option", { value: v, children: [v, "%"] }, v)) }) }), _jsx("td", { className: "py-1.5 text-right", children: formatCents(c.line_total_cents) }), _jsx("td", { className: "py-1 text-right print:hidden", children: _jsx("button", { type: "button", "aria-label": "Regel verwijderen", onClick: () => onLinesChange?.(safeLines.filter((_, idx) => idx !== i)), className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) })] })) : (_jsxs(_Fragment, { children: [_jsx("td", { className: "whitespace-pre-wrap break-words py-2 pr-3", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "min-w-0 flex-1", children: line.description || "—" }), isBasisLine(line) ? _jsx(BasisBadge, {}) : null, line.articleId ? _jsx(ArticlePhoto, { src: articleById.get(line.articleId)?.photo_url ?? line.photoUrl, alt: line.description, className: "h-9 w-9" }) : null] }) }), _jsxs("td", { className: "py-2 text-right", children: [c.quantity, " ", line.unit] }), _jsx("td", { className: "py-2 text-right", children: formatCents(c.unit_price_cents) }), _jsxs("td", { className: "py-2 text-right", children: [c.vat_rate, "%"] }), _jsx("td", { className: "py-2 text-right", children: formatCents(c.line_total_cents) })] })) }, i));
102
+ }) : _jsx("tr", { children: _jsx("td", { colSpan: editable ? 6 : 5, className: "py-8 text-center text-muted-foreground", children: "Nog geen factuurregels" }) }) })] }), editable ? _jsx(AddLineBar, { defaultVat: reverseCharge ? "0" : defaultVat, onAdd: (l) => onLinesChange?.([...safeLines, l]) }) : null, _jsxs("div", { className: "mt-6 ml-auto w-64 space-y-1.5", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Totaal excl. btw" }), _jsx("span", { children: formatCents(subtotal) })] }), editable && onReverseChargeChange ? (_jsxs("div", { className: "flex items-center justify-between gap-2", "data-html2canvas-ignore": true, children: [_jsx("span", { children: "Btw factuur" }), _jsx("div", { className: "flex overflow-hidden rounded-md border border-border", children: [...VAT_OPTIONS, "verlegd"].map((c) => {
99
103
  const active = c === "verlegd" ? reverseCharge : !reverseCharge && computed.length > 0 && computed.every((l) => String(l.vat_rate) === c);
100
104
  return (_jsx("button", { type: "button", onClick: () => { onReverseChargeChange(c === "verlegd"); onLinesChange?.(applyVatChoice(safeLines, c)); }, className: `px-2 py-0.5 text-xs ${active ? "bg-primary text-primary-foreground" : "bg-transparent hover:bg-muted"}`, children: c === "verlegd" ? "Verlegd" : `${c}%` }, c));
101
105
  }) })] })) : null, reverseCharge ? (_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { children: "Btw verlegd" }), _jsx("span", { children: formatCents(0) })] })) : (vatGroups.length ? vatGroups : [{ rate: Number(defaultVat) || 0, vat: 0 }]).map((g) => (_jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { children: ["Btw ", g.rate, "%"] }), _jsx("span", { children: formatCents(g.vat) })] }, g.rate))), _jsxs("div", { className: "flex justify-between border-t border-foreground pt-2 text-sm font-semibold", children: [_jsx("span", { children: "Door u te betalen" }), _jsx("span", { children: formatCents(subtotal + (reverseCharge ? 0 : vatTotal)) })] }), reverseCharge ? _jsxs("p", { className: "pt-1 text-xs", children: ["Btw verlegd naar afnemer", details.customer_vat_number ? ` (btw-nr. ${details.customer_vat_number})` : ""] }) : null] }), _jsxs("p", { className: "mt-auto pb-5 pt-8 text-center font-medium", children: ["Graag betaling binnen ", details.payment_term_days || branding.payment_term_days, " dagen o.v.v. ", number] })] }), _jsxs("footer", { className: "grid min-h-16 grid-cols-3 items-center gap-4 bg-[var(--invoice-secondary)] px-8 py-4 text-[10px] text-primary-foreground", children: [_jsxs("span", { children: ["KvK ", branding.chamber_of_commerce_number || "—"] }), _jsxs("span", { className: "text-center", children: ["Btw-id ", branding.vat_identification_number || "—"] }), _jsxs("span", { className: "text-right", children: ["IBAN ", branding.iban || "—"] })] })] }), _jsx("style", { children: `@page{size:A4;margin:0}@media print{body *{visibility:hidden!important}.invoice-print-target,.invoice-print-target *{visibility:visible!important}.invoice-print-target{position:absolute!important;inset:0!important;width:210mm!important;min-height:297mm!important;height:auto!important;max-width:none!important;box-shadow:none!important;print-color-adjust:exact!important;-webkit-print-color-adjust:exact!important}.invoice-print-target tr{break-inside:avoid}}` })] }));
@@ -0,0 +1,2 @@
1
+ export declare function InvoiceModuleSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=InvoiceModuleSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvoiceModuleSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/InvoiceModuleSettingsCard.tsx"],"names":[],"mappings":"AAKA,wBAAgB,yBAAyB,gCAiFxC"}
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Switch, toast } from "@flowselections/core";
4
+ import { Loader2, Percent, Plus } from "lucide-react";
5
+ import { fetchAllVatRates, saveTimezone, saveVatRate, useInvoiceSettings } from "../../lib/settings";
6
+ export function InvoiceModuleSettingsCard() {
7
+ const settings = useInvoiceSettings();
8
+ const [tz, setTz] = useState(settings.timezone);
9
+ const [rates, setRates] = useState([]);
10
+ const [loadError, setLoadError] = useState(null);
11
+ const [busy, setBusy] = useState(null);
12
+ const [newRate, setNewRate] = useState("");
13
+ const [newDesc, setNewDesc] = useState("");
14
+ const reload = async () => {
15
+ try {
16
+ setRates(await fetchAllVatRates());
17
+ setLoadError(null);
18
+ }
19
+ catch (e) {
20
+ const m = e instanceof Error ? e.message : "Laden mislukt";
21
+ console.error("[facturen] btw-tarieven", e);
22
+ setLoadError(m);
23
+ }
24
+ };
25
+ useEffect(() => { void reload(); }, []);
26
+ useEffect(() => { setTz(settings.timezone); }, [settings.timezone]);
27
+ const run = async (key, fn, ok) => {
28
+ if (busy)
29
+ return;
30
+ setBusy(key);
31
+ try {
32
+ await fn();
33
+ await reload();
34
+ toast.success(ok);
35
+ }
36
+ catch (e) {
37
+ console.error("[facturen] instellingen", e);
38
+ toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
39
+ }
40
+ finally {
41
+ setBusy(null);
42
+ }
43
+ };
44
+ const addRate = () => {
45
+ const n = Number(newRate.replace(",", "."));
46
+ if (!Number.isFinite(n) || n <= 0 || n >= 100) {
47
+ toast.error("Vul een btw-percentage tussen 0 en 100 in");
48
+ return;
49
+ }
50
+ void run("new", async () => { await saveVatRate({ rate: n, description: newDesc, is_default: false, active: true }); setNewRate(""); setNewDesc(""); }, "Btw-tarief toegevoegd");
51
+ };
52
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Percent, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Btw en tijdzone" })] }), _jsx(CardDescription, { children: "Btw-tarieven voor factuurregels en de tijdzone voor factuurdatums en periodes. \"Btw verlegd\" is altijd beschikbaar." })] }), _jsxs(CardContent, { className: "space-y-5", children: [settings.usingFallback && settings.warnings.length ? (_jsx("p", { className: "rounded-md border border-destructive/40 p-2 text-xs text-destructive", children: settings.warnings.join(" ") })) : null, _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "invoice-tz", children: "Tijdzone" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: "invoice-tz", value: tz, onChange: (e) => setTz(e.target.value), placeholder: "Europe/Amsterdam" }), _jsx(Button, { onClick: () => void run("tz", () => saveTimezone(tz.trim()), "Tijdzone opgeslagen"), disabled: Boolean(busy) || !tz.trim() || tz.trim() === settings.timezone, children: busy === "tz" ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : "Opslaan" })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Btw-tarieven" }), loadError ? _jsxs("p", { className: "text-xs text-destructive", children: ["Btw-tarieven niet beschikbaar: ", loadError] }) : null, _jsx("div", { className: "space-y-2", children: rates.map((r) => (_jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-md border border-border p-2 text-sm", children: [_jsxs("span", { className: "w-12 font-medium tabular-nums", children: [r.rate, "%"] }), _jsx("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", children: r.description || "—" }), _jsxs("label", { className: "flex items-center gap-1 text-xs", children: [_jsx("input", { type: "radio", name: "invoice-vat-default", checked: r.is_default, disabled: Boolean(busy) || !r.active, onChange: () => void run(`d-${r.id}`, () => saveVatRate({ ...r, is_default: true }), `${r.rate}% is nu standaard`) }), "Standaard"] }), _jsxs("label", { className: "flex items-center gap-1 text-xs", children: [_jsx(Switch, { checked: r.active, disabled: Boolean(busy) || r.is_default, onCheckedChange: (v) => void run(`a-${r.id}`, () => saveVatRate({ ...r, active: v }), v ? "Tarief geactiveerd" : "Tarief uitgezet") }), "Actief"] })] }, r.id ?? r.rate))) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { value: newRate, onChange: (e) => setNewRate(e.target.value), placeholder: "%", className: "w-20", inputMode: "decimal" }), _jsx(Input, { value: newDesc, onChange: (e) => setNewDesc(e.target.value), placeholder: "Omschrijving", maxLength: 60 }), _jsx(Button, { variant: "outline", onClick: addRate, disabled: Boolean(busy) || !newRate.trim(), children: busy === "new" ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : _jsx(Plus, { className: "h-4 w-4" }) })] })] })] })] }));
53
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ModuleHealthSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/ModuleHealthSettingsCard.tsx"],"names":[],"mappings":"AAsBA,mFAAmF;AACnF,wBAAgB,wBAAwB,gCAmDvC"}
1
+ {"version":3,"file":"ModuleHealthSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/ModuleHealthSettingsCard.tsx"],"names":[],"mappings":"AAuBA,mFAAmF;AACnF,wBAAgB,wBAAwB,gCAoDvC"}
@@ -4,6 +4,7 @@ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, supa
4
4
  import { CheckCircle2, AlertTriangle, RefreshCw } from "lucide-react";
5
5
  import { STORAGE_BUCKETS } from "../../lib/config";
6
6
  import { isMissingObjectError } from "../../lib/dependencies";
7
+ import { loadInvoiceSettings } from "../../lib/settings";
7
8
  import { fetchInvoiceBranding, fetchInvoiceEmailPurpose, fetchSenderForPurpose } from "../../lib/facturen";
8
9
  const db = supabase;
9
10
  async function tableCheck(table, label, module) {
@@ -33,6 +34,7 @@ export function ModuleHealthSettingsCard() {
33
34
  return { label: "Afzender factuurmail", ok: Boolean(s?.id), hint: `Geen mailbox gekoppeld aan '${p}'.` };
34
35
  })
35
36
  .catch((e) => ({ label: "Afzender factuurmail", ok: false, hint: isMissingObjectError(e) ? "Module E-mail - Authenticatie ontbreekt." : "Niet bereikbaar." })),
37
+ loadInvoiceSettings(true).then((st) => ({ label: "Btw-tarieven en tijdzone", ok: !st.usingFallback, hint: `${st.warnings.join(" ")} Standaardwaarden worden gebruikt.` })),
36
38
  bucketCheck(STORAGE_BUCKETS.logos, "Opslag logo"),
37
39
  bucketCheck(STORAGE_BUCKETS.articlePhotos, "Opslag artikelfoto's"),
38
40
  tableCheck("artikelen_instellingen", "Marge-instelling", "Artikelen"),
@@ -1,6 +1,8 @@
1
1
  import type { FlowModule } from "@flowselections/core";
2
2
  import { InvoiceEmailSettingsCard } from "./components/settings/InvoiceEmailSettingsCard";
3
3
  import { ModuleHealthSettingsCard } from "./components/settings/ModuleHealthSettingsCard";
4
+ import { InvoiceModuleSettingsCard } from "./components/settings/InvoiceModuleSettingsCard";
5
+ export { InvoiceModuleSettingsCard };
4
6
  export { ModuleHealthSettingsCard };
5
7
  export { ModuleErrorView } from "./components/ModuleErrorView";
6
8
  export { InvoiceEmailSettingsCard };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gDAAgD,CAAC;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,gDAAgD,CAAC;AAC1F,OAAO,EAAE,wBAAwB,EAAE,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,8BAA8B,EAAE,MAAM,6CAA6C,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAErE,eAAO,MAAM,iCAAiC,EAAE,UAgB/C,CAAC;AACF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,2BAA2B,EAAE,MAAM,mDAAmD,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gDAAgD,CAAC;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,gDAAgD,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,wBAAwB,EAAE,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,8BAA8B,EAAE,MAAM,6CAA6C,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAErE,eAAO,MAAM,iCAAiC,EAAE,UAiB/C,CAAC;AACF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,2BAA2B,EAAE,MAAM,mDAAmD,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC"}
package/dist-lib/index.js CHANGED
@@ -5,6 +5,8 @@ import { FileText } from "lucide-react";
5
5
  import { InvoiceBrandingSettingsCard } from "./components/settings/InvoiceBrandingSettingsCard";
6
6
  import { InvoiceEmailSettingsCard } from "./components/settings/InvoiceEmailSettingsCard";
7
7
  import { ModuleHealthSettingsCard } from "./components/settings/ModuleHealthSettingsCard";
8
+ import { InvoiceModuleSettingsCard } from "./components/settings/InvoiceModuleSettingsCard";
9
+ export { InvoiceModuleSettingsCard };
8
10
  export { ModuleHealthSettingsCard };
9
11
  export { ModuleErrorView } from "./components/ModuleErrorView";
10
12
  export { InvoiceEmailSettingsCard };
@@ -15,7 +17,7 @@ export { TemplateModuleLogo } from './components/TemplateModuleLogo';
15
17
  export const facturenInstallatietechniekModule = {
16
18
  id: "facturen-installatietechniek",
17
19
  name: "Facturen Installatietechniek",
18
- version: "1.4.4", // houd gelijk aan package.json (zie src/lib/module-version.test.ts)
20
+ version: "1.5.0", // houd gelijk aan package.json (zie src/lib/module-version.test.ts)
19
21
  nav: {
20
22
  label: "Facturen",
21
23
  href: "/facturen-installatietechniek",
@@ -24,7 +26,8 @@ export const facturenInstallatietechniekModule = {
24
26
  settingsCards: [
25
27
  { component: InvoiceBrandingSettingsCard, order: 20 },
26
28
  { component: InvoiceEmailSettingsCard, order: 21 },
27
- { component: ModuleHealthSettingsCard, order: 22 },
29
+ { component: InvoiceModuleSettingsCard, order: 22 },
30
+ { component: ModuleHealthSettingsCard, order: 23 },
28
31
  ],
29
32
  };
30
33
  export { InvoiceDetailsFields } from "./components/InvoiceDetailsFields";