@flowselections/facturen-installatie-techniek 1.4.3 → 1.4.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AAiJA,wBAAgB,8BAA8B,gCAge7C"}
1
+ {"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AAmLA,wBAAgB,8BAA8B,gCAwiB7C"}
@@ -7,7 +7,7 @@ import { supabase } from "@flowselections/core";
7
7
  import { Link, useNavigate } from "@tanstack/react-router";
8
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";
9
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";
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
11
  import { InvoiceA4Preview } from "./InvoiceA4Preview";
12
12
  function statusVariant(status) {
13
13
  if (status === "betaald")
@@ -18,13 +18,24 @@ function statusVariant(status) {
18
18
  return "outline";
19
19
  return "secondary";
20
20
  }
21
- function InvoiceTable({ invoices, emptyText, onOpen, }) {
21
+ function InvoiceTable({ invoices, emptyText, onOpen, selection, }) {
22
22
  if (invoices.length === 0) {
23
23
  return _jsx("p", { className: "text-sm text-muted-foreground", children: emptyText });
24
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))) })] }));
25
+ return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [selection && _jsx(TableHead, { className: "w-10" }), _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) => {
26
+ const checked = selection?.selectedIds.has(invoice.id) ?? false;
27
+ const selectable = selection ? checked || selection.isSelectable(invoice) : true;
28
+ return (_jsxs(TableRow, { className: selection && !selectable ? "cursor-not-allowed opacity-40" : "cursor-pointer", onClick: () => {
29
+ if (selection) {
30
+ if (selectable)
31
+ selection.onToggle(invoice);
32
+ }
33
+ else
34
+ onOpen(invoice.id);
35
+ }, 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
+ ? 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
+ }) })] }));
28
39
  }
29
40
  export function FacturenForInstallatieTechniek() {
30
41
  const navigate = useNavigate();
@@ -41,6 +52,36 @@ export function FacturenForInstallatieTechniek() {
41
52
  const [open, setOpen] = useState(false);
42
53
  const [saving, setSaving] = useState(false);
43
54
  const createRef = useRef(false);
55
+ // samenvoegen
56
+ const [mergeMode, setMergeMode] = useState(false);
57
+ const [mergeSelected, setMergeSelected] = useState([]);
58
+ const [mergeConfirm, setMergeConfirm] = useState(false);
59
+ const [merging, setMerging] = useState(false);
60
+ const mergeRef = useRef(false);
61
+ const mergeIds = useMemo(() => new Set(mergeSelected.map((i) => i.id)), [mergeSelected]);
62
+ 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);
66
+ const doMerge = async () => {
67
+ if (!canMerge(mergeSelected) || mergeRef.current)
68
+ return;
69
+ mergeRef.current = true;
70
+ setMerging(true);
71
+ try {
72
+ const id = await mergeInvoices(mergeSelected.map((i) => i.id));
73
+ toast.success(`${mergeSelected.length} facturen samengevoegd`);
74
+ stopMerge();
75
+ await navigate({ to: "/facturen-installatietechniek/detail/$id", params: { id }, search: { send: true } });
76
+ }
77
+ catch (e) {
78
+ toast.error(e instanceof Error ? e.message : "Samenvoegen mislukt");
79
+ }
80
+ finally {
81
+ mergeRef.current = false;
82
+ setMerging(false);
83
+ }
84
+ };
44
85
  // bron-tab
45
86
  const [sourceType, setSourceType] = useState("work_order");
46
87
  const [sources, setSources] = useState([]);
@@ -249,7 +290,11 @@ export function FacturenForInstallatieTechniek() {
249
290
  setSaving(false);
250
291
  }
251
292
  };
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) => {
293
+ 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).filter(([value]) => value !== "samengevoegd").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)))] })] }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [mergeMode ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", onClick: stopMerge, disabled: merging, children: "Annuleren" }), _jsxs(Button, { onClick: () => setMergeConfirm(true), disabled: !canMerge(mergeSelected) || merging, children: ["Samenvoegen (", mergeSelected.length, ")"] })] })) : (_jsx(Button, { variant: "outline", onClick: () => setMergeMode(true), children: "Samenvoegen" })), _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" }) }), _jsxs(CardContent, { children: [_jsx(InvoiceTable, { invoices: conceptInvoices, emptyText: "Geen conceptfacturen.", onOpen: openInvoice, selection: mergeMode ? {
294
+ selectedIds: mergeIds,
295
+ isSelectable: (i) => canSelectForMerge(i, mergeSelected),
296
+ onToggle: toggleMerge,
297
+ } : undefined }), mergeMode && (_jsx("p", { className: "mt-3 text-xs text-muted-foreground", children: "Vink minimaal twee conceptfacturen van dezelfde klant aan." }))] })] }), _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: mergeConfirm, onOpenChange: (o) => !merging && setMergeConfirm(o), children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Facturen samenvoegen" }) }), _jsxs("p", { className: "text-sm", children: [mergeSelected.length, " conceptfacturen van ", mergeSelected[0]?.customer_name ?? "deze klant", " worden samengevoegd tot \u00E9\u00E9n nieuwe factuur. De losse facturen worden bewaard als \u201CSamengevoegd\u201D en verdwijnen uit de lijst."] }), _jsx("ul", { className: "text-sm text-muted-foreground", children: mergeSelected.map((i) => (_jsxs("li", { children: [i.invoice_number ?? "—", " \u00B7 ", formatCents(i.total_cents)] }, i.id))) }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => setMergeConfirm(false), disabled: merging, children: "Annuleren" }), _jsx(Button, { onClick: () => void doMerge(), disabled: merging, children: merging ? "Bezig…" : "Samenvoegen" })] })] }) }), _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
298
  setPickedCustomerId(c.id);
254
299
  setDetails((d) => ({
255
300
  ...d,
@@ -1 +1 @@
1
- {"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,iBAAiB,gCA6bhC"}
1
+ {"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AA8CA,wBAAgB,iBAAiB,gCA2chC"}
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { DEFAULT_VAT_RATE } from "../lib/config";
4
4
  import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
5
- import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, toast, } from "@flowselections/core";
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, SOURCE_LABELS, STATUS_LABELS, updateInvoiceFull, updateInvoiceStatus, } 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, } 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());
@@ -34,8 +34,11 @@ export function FactuurDetailPage() {
34
34
  const sheetRef = useRef(null);
35
35
  const autoOpenedRef = useRef(false);
36
36
  const sendingRef = useRef(false);
37
- const statusRef = useRef(false);
38
37
  const [senderChecked, setSenderChecked] = useState(false);
38
+ const [noteText, setNoteText] = useState("");
39
+ const [reverseCharge, setReverseCharge] = useState(false);
40
+ const [noteSaving, setNoteSaving] = useState(false);
41
+ const [noteSavedAt, setNoteSavedAt] = useState(null);
39
42
  const loadSeqRef = useRef(0);
40
43
  const load = useCallback(async () => {
41
44
  const seq = ++loadSeqRef.current;
@@ -48,6 +51,9 @@ export function FactuurDetailPage() {
48
51
  if (seq !== loadSeqRef.current)
49
52
  return;
50
53
  setInvoice(inv);
54
+ setNoteText(inv?.note ?? "");
55
+ setReverseCharge(Boolean(inv?.vat_reverse_charge));
56
+ setNoteSavedAt(null);
51
57
  setDetails(detailsFromRecord(inv));
52
58
  const safeRows = Array.isArray(rows) ? rows : [];
53
59
  setLines(safeRows.map((r) => itemToEditLine(r, Number(inv?.vat_rate ?? DEFAULT_VAT_RATE))));
@@ -105,37 +111,38 @@ export function FactuurDetailPage() {
105
111
  search: { send: false },
106
112
  });
107
113
  };
108
- const changeStatus = async (status) => {
109
- if (statusRef.current)
110
- return;
111
- statusRef.current = true;
114
+ const save = async () => {
112
115
  setSaving(true);
113
116
  try {
114
- await updateInvoiceStatus(id, status);
115
- toast.success("Status bijgewerkt");
117
+ await updateInvoiceFull(id, details, lines, invoice?.note ?? null, reverseCharge);
118
+ toast.success("Factuur opgeslagen");
119
+ setEditing(false);
116
120
  await load();
117
121
  }
118
- catch {
119
- toast.error("Status bijwerken mislukt");
122
+ catch (e) {
123
+ toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
120
124
  }
121
125
  finally {
122
- statusRef.current = false;
123
126
  setSaving(false);
124
127
  }
125
128
  };
126
- const save = async () => {
127
- setSaving(true);
129
+ const saveNote = async () => {
130
+ if (noteSaving || !invoice)
131
+ return;
132
+ setNoteSaving(true);
128
133
  try {
129
- await updateInvoiceFull(id, details, lines, invoice?.note ?? null);
130
- toast.success("Factuur opgeslagen");
131
- setEditing(false);
132
- await load();
134
+ const at = await setInvoiceNote(id, noteText);
135
+ const clean = noteText.trim();
136
+ setInvoice((prev) => (prev ? { ...prev, note: clean || null } : prev));
137
+ setNoteText(clean);
138
+ setNoteSavedAt(at ?? new Date().toISOString());
139
+ toast.success("Notitie opgeslagen");
133
140
  }
134
141
  catch (e) {
135
- toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
142
+ toast.error(e instanceof Error ? e.message : "Notitie opslaan mislukt");
136
143
  }
137
144
  finally {
138
- setSaving(false);
145
+ setNoteSaving(false);
139
146
  }
140
147
  };
141
148
  const pdfFilename = `Factuur-${invoice?.invoice_number ?? "concept"}.pdf`;
@@ -274,5 +281,5 @@ export function FactuurDetailPage() {
274
281
  }
275
282
  const isConcept = invoice.status === "concept";
276
283
  const wasSent = Boolean(invoice.sent_at);
277
- return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "sticky top-16 z-20 -mx-6 flex flex-wrap items-center gap-3 border-b border-border/60 bg-background px-6 py-3", children: [_jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsxs(Link, { to: "/facturen-installatietechniek", className: "gap-1", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Overzicht"] }) }), _jsx("h1", { className: "text-xl font-semibold", children: invoice.invoice_number ?? "Concept" }), _jsxs(Badge, { variant: "outline", children: [SOURCE_LABELS[invoice.source_type] ?? invoice.source_type, invoice.source_number ? ` ${invoice.source_number}` : ""] }), invoice.sent_at ? (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Verzonden ", new Date(invoice.sent_at).toLocaleString("nl-NL"), " aan ", invoice.sent_to] })) : null, _jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-1 rounded-full border border-border bg-card p-1", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 rounded-full", "aria-label": "Vorige factuur op de lijst", title: "Vorige factuur op de lijst", disabled: editing || loading || siblingsLoading || !prevInvoice, onClick: () => goToSibling(prevInvoice?.id), children: _jsx(ChevronLeft, { className: "h-5 w-5" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 rounded-full", "aria-label": "Volgende factuur op de lijst", title: "Volgende factuur op de lijst", disabled: editing || loading || siblingsLoading || !nextInvoice, onClick: () => goToSibling(nextInvoice?.id), children: _jsx(ChevronRight, { className: "h-5 w-5" }) })] }), editing ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: () => { setEditing(false); void load(); }, disabled: saving, children: [_jsx(X, { className: "mr-2 h-4 w-4" }), "Annuleren"] }), _jsxs(Button, { onClick: save, disabled: saving, children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), saving ? "Opslaan…" : "Opslaan"] })] })) : (_jsxs(_Fragment, { children: [!wasSent ? (_jsxs(Button, { variant: "outline", onClick: () => (isConcept ? setEditing(true) : toast.error("Alleen conceptfacturen kunnen worden bewerkt. Zet de status terug op Concept.")), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Bewerken"] })) : null, _jsxs(Button, { onClick: () => void openSend(), children: [_jsx(Mail, { className: "mr-2 h-4 w-4" }), wasSent ? "Opnieuw verzenden" : "Verzenden"] })] })), _jsx("div", { className: "w-44", children: _jsxs(Select, { value: invoice.status, onValueChange: (v) => changeStatus(v), disabled: saving || editing, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.entries(STATUS_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] }) })] })] }), _jsx("div", { children: _jsx("div", { className: "min-w-0", children: _jsx(InvoiceA4Preview, { ref: sheetRef, branding: invoice.branding_snapshot ?? emptyInvoiceBranding(), details: details, lines: lines, invoiceNumber: invoice.invoice_number, logoUrl: logoUrl, printable: !editing, editable: editing, defaultVat: String(Number(invoice.vat_rate) || DEFAULT_VAT_RATE), onDetailsChange: setDetails, onLinesChange: setLines }) }) }), _jsx(Dialog, { open: sendOpen, onOpenChange: changeSendOpen, children: _jsxs(DialogContent, { className: "flex max-h-[92vh] max-w-[calc(100vw-2rem)] flex-col sm:max-w-lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Factuur verzenden" }) }), _jsxs("div", { className: "min-h-0 flex-1 space-y-3 overflow-y-auto pr-1", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "mail-to", children: "E-mailadres klant" }), _jsx(Input, { id: "mail-to", type: "email", value: mail.to, onChange: (e) => setMail({ ...mail, to: e.target.value }), onBlur: () => setMailToTouched(true), placeholder: "naam@bedrijf.nl", "aria-invalid": mailToTouched && !isValidEmail(mail.to) }), mailToTouched && !isValidEmail(mail.to) ? (_jsx("p", { className: "text-xs text-destructive", children: "Vul een geldig e-mailadres in, bijvoorbeeld naam@bedrijf.nl" })) : null] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "mail-subject", children: "Onderwerp" }), _jsx(Input, { id: "mail-subject", value: mail.subject, onChange: (e) => setMail({ ...mail, subject: e.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "mail-msg", children: "Bericht" }), _jsx(Textarea, { id: "mail-msg", rows: 6, value: mail.message, onChange: (e) => setMail({ ...mail, message: e.target.value }) })] }), _jsxs("label", { className: "flex cursor-pointer items-start gap-3 rounded-md border border-border p-3", children: [_jsx("input", { type: "checkbox", checked: attachPdf, onChange: (event) => setAttachPdf(event.target.checked), className: "mt-1 h-4 w-4 accent-primary" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium", children: [preparedPdf ? _jsx(FileCheck2, { className: "h-4 w-4 text-primary" }) : _jsx(Download, { className: "h-4 w-4" }), "Factuur als PDF meesturen"] }), _jsx("span", { className: "mt-1 block truncate text-xs text-muted-foreground", children: preparingPdf ? "PDF wordt aangemaakt en gedownload…" : preparedPdf ? `${pdfFilename} is gedownload en klaar als bijlage.` : "PDF kon niet worden voorbereid." })] })] }), !attachPdf ? _jsx("p", { className: "text-xs text-destructive", children: "Vink de PDF aan om deze e-mail te kunnen versturen." }) : null, _jsxs("p", { className: "text-xs text-muted-foreground", children: [!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" })] })] }) })] }));
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" })] })] }) })] }));
278
285
  }
@@ -13,6 +13,9 @@ type Props = {
13
13
  onLinesChange?: (l: EditLine[]) => void;
14
14
  /** Toon klantkeuzelijst bij Klantnaam (alleen nieuwe handmatige factuur). */
15
15
  onCustomerPick?: (c: CustomerOption) => void;
16
+ /** Btw verlegd naar afnemer: alle regels 0%, totaal zonder btw. */
17
+ reverseCharge?: boolean;
18
+ onReverseChargeChange?: (v: boolean) => void;
16
19
  };
17
20
  export declare const InvoiceA4Preview: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLElement>>;
18
21
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"InvoiceA4Preview.d.ts","sourceRoot":"","sources":["../../src/components/InvoiceA4Preview.tsx"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EAEpB,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;CAC9C,CAAC;AAqDF,eAAO,MAAM,gBAAgB,+FA4K3B,CAAC"}
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"}
@@ -4,8 +4,8 @@ import { DEFAULT_VAT_RATE, SEARCH_DEBOUNCE_MS, VAT_OPTIONS as CONFIG_VAT_OPTIONS
4
4
  import { dependencyMessage } from "../lib/dependencies";
5
5
  import { useServerFn } from "@tanstack/react-start";
6
6
  import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "@flowselections/core";
7
- import { ChevronDown, Package, Plus, Printer, Search, Trash2 } from "lucide-react";
8
- import { editLineToBillable, emptyEditLine, searchCustomers, fetchPackages, formatCents, } from "../lib/facturen";
7
+ 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
9
  import { listInvoiceArticles } from "../lib/articles.functions";
10
10
  import { ArticlePhoto } from "./ArticlePhoto";
11
11
  const VAT_OPTIONS = CONFIG_VAT_OPTIONS;
@@ -38,7 +38,7 @@ 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 }, ref) {
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) {
42
42
  const safeLines = Array.isArray(lines) ? lines : [];
43
43
  const listArticles = useServerFn(listInvoiceArticles);
44
44
  const [articles, setArticles] = useState([]);
@@ -68,12 +68,37 @@ export const InvoiceA4Preview = forwardRef(function InvoiceA4Preview({ branding,
68
68
  const contact = [branding.address, [branding.postal_code, branding.city].filter(Boolean).join(" "), branding.email, branding.phone, branding.website].filter(Boolean);
69
69
  const setD = (k) => (v) => onDetailsChange?.({ ...details, [k]: v });
70
70
  const setL = (i, patch) => onLinesChange?.(safeLines.map((l, idx) => (idx === i ? { ...l, ...patch } : l)));
71
+ const [pkgMap, setPkgMap] = useState(new Map());
72
+ useEffect(() => {
73
+ let active = true;
74
+ fetchPackages().then((list) => { if (active)
75
+ setPkgMap(new Map((Array.isArray(list) ? list : []).map((p) => [p.id, p]))); }).catch(() => { });
76
+ return () => { active = false; };
77
+ }, []);
78
+ const basisOf = (line) => (line.packageId ? pkgMap.get(line.packageId) : undefined);
79
+ const isBasisLine = (line) => Boolean(line.isBasis || isBasisPackage(basisOf(line)));
80
+ const stepQuarter = (i, delta) => {
81
+ const line = safeLines[i];
82
+ const pkg = basisOf(line);
83
+ const { base, quarters } = splitQuarterDescription(line.description);
84
+ const current = line.extraQuarters ?? quarters;
85
+ const next = Math.max(0, current + delta);
86
+ if (next === current)
87
+ return;
88
+ const baseCents = Number(pkg?.unit_price_cents) || 0;
89
+ const qc = Number(pkg?.extra_quarter_price_cents) || 0;
90
+ setL(i, { ...basisLinePatch(base, baseCents, qc, next), isBasis: true });
91
+ };
92
+ const BasisBadge = () => _jsx("span", { className: "ml-1 inline-block shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium uppercase text-primary", children: "Basis" });
71
93
  const F = ({ k, placeholder, type, className }) => editable ? (_jsx("input", { type: type ?? "text", value: details[k], placeholder: placeholder, onChange: (e) => setD(k)(e.target.value), className: `w-full rounded-sm border border-dashed border-border bg-transparent px-1 py-0.5 outline-none focus:border-primary ${className ?? ""}` })) : (_jsx("span", { className: className, children: details[k] || (type === "date" ? "—" : placeholder) }));
72
94
  const fmtDate = (v) => (v ? new Date(`${v}T12:00:00`).toLocaleDateString("nl-NL") : "—");
73
95
  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) => {
74
96
  const c = computed[i];
75
- 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), 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" }), 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: _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: Boolean(line.articleId), 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), 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 || "—" }), 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));
76
- }) : _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: 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) })] }), (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 + vatTotal) })] })] }), _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}}` })] }));
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) => {
99
+ const active = c === "verlegd" ? reverseCharge : !reverseCharge && computed.length > 0 && computed.every((l) => String(l.vat_rate) === c);
100
+ 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
+ }) })] })) : 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}}` })] }));
77
102
  });
78
103
  function AddLineBar({ defaultVat, onAdd }) {
79
104
  const searchArticles = useServerFn(listInvoiceArticles);
@@ -137,6 +162,12 @@ function AddLineBar({ defaultVat, onAdd }) {
137
162
  setArticleQuery("");
138
163
  }, children: [_jsx(ArticlePhoto, { src: article.photo_url, alt: article.name, className: "h-24 w-24" }), _jsxs("span", { className: "w-24", children: [_jsx("span", { className: "line-clamp-2 block whitespace-normal font-medium leading-snug", children: article.name }), _jsxs("span", { className: "mt-1 block truncate text-xs text-muted-foreground", children: ["EAN ", article.gtin || "—"] }), _jsx("span", { className: "mt-1 block font-semibold", children: formatCents(article.sales_price_cents) })] })] }, article.id))) })] })] }), _jsxs(Popover, { open: packageOpen, onOpenChange: setPackageOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", size: "sm", variant: "outline", children: [_jsx(Package, { className: "mr-1 h-4 w-4" }), "Pakket"] }) }), _jsxs(PopoverContent, { className: "w-96 p-2", align: "start", children: [_jsx(Input, { autoFocus: true, placeholder: "Zoek pakket\u2026", value: packageQuery, onChange: (e) => setPackageQuery(e.target.value) }), _jsx("div", { className: "mt-2 max-h-72 overflow-y-auto", children: packagesError ? _jsx("p", { className: "p-2 text-sm text-destructive", children: packagesError }) : filteredPackages.length === 0 ? _jsx("p", { className: "p-2 text-sm text-muted-foreground", children: "Geen pakketten gevonden." }) : filteredPackages.map((p) => (_jsxs(Button, { type: "button", variant: "ghost", className: "flex h-auto w-full items-start justify-between gap-2 px-2 py-1.5 text-left text-sm", onClick: () => {
139
164
  const cents = Math.round(Number(p.unit_price_cents) || 0);
165
+ if (isBasisPackage(p)) {
166
+ onAdd({ ...basisLinePatch(p.name, cents, Number(p.extra_quarter_price_cents) || 0, 0), unit: "pakket", vat: defaultVat, kind: "package", packageId: p.id, isBasis: true });
167
+ setPackageOpen(false);
168
+ setPackageQuery("");
169
+ return;
170
+ }
140
171
  onAdd({
141
172
  description: p.description?.trim() ? `${p.name} — ${p.description.trim()}` : p.name,
142
173
  quantity: String(Number(p.default_quantity) > 0 ? Number(p.default_quantity) : 1),
@@ -148,7 +179,7 @@ function AddLineBar({ defaultVat, onAdd }) {
148
179
  });
149
180
  setPackageOpen(false);
150
181
  setPackageQuery("");
151
- }, children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-medium", children: p.name }), p.code ? _jsx("span", { className: "block text-xs text-muted-foreground", children: p.code }) : null] }), _jsx("span", { className: "shrink-0", children: formatCents(p.unit_price_cents) })] }, p.id))) })] })] })] }));
182
+ }, children: [_jsxs("span", { className: "min-w-0", children: [_jsxs("span", { className: "flex items-center truncate font-medium", children: [p.name, isBasisPackage(p) ? _jsx("span", { className: "ml-1 rounded bg-primary/15 px-1.5 py-0.5 text-[10px] uppercase text-primary", children: "Basis" }) : null] }), p.code ? _jsx("span", { className: "block text-xs text-muted-foreground", children: p.code }) : null] }), _jsx("span", { className: "shrink-0", children: formatCents(p.unit_price_cents) })] }, p.id))) })] })] })] }));
152
183
  }
153
184
  function formatDueDate(invoiceDate, paymentTerm) {
154
185
  if (!invoiceDate)
package/dist-lib/index.js CHANGED
@@ -15,7 +15,7 @@ export { TemplateModuleLogo } from './components/TemplateModuleLogo';
15
15
  export const facturenInstallatietechniekModule = {
16
16
  id: "facturen-installatietechniek",
17
17
  name: "Facturen Installatietechniek",
18
- version: "1.4.0", // houd gelijk aan package.json (zie src/lib/module-version.test.ts)
18
+ version: "1.4.4", // houd gelijk aan package.json (zie src/lib/module-version.test.ts)
19
19
  nav: {
20
20
  label: "Facturen",
21
21
  href: "/facturen-installatietechniek",