@flowselections/facturen-installatie-techniek 1.0.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist-lib/components/ArticlePhoto.d.ts +8 -0
  2. package/dist-lib/components/ArticlePhoto.d.ts.map +1 -0
  3. package/dist-lib/components/ArticlePhoto.js +7 -0
  4. package/dist-lib/components/FacturenForInstallatieTechniek.d.ts.map +1 -1
  5. package/dist-lib/components/FacturenForInstallatieTechniek.js +143 -50
  6. package/dist-lib/components/FactuurDetailPage.d.ts.map +1 -1
  7. package/dist-lib/components/FactuurDetailPage.js +224 -13
  8. package/dist-lib/components/InvoiceA4Preview.d.ts +19 -0
  9. package/dist-lib/components/InvoiceA4Preview.d.ts.map +1 -0
  10. package/dist-lib/components/InvoiceA4Preview.js +118 -0
  11. package/dist-lib/components/InvoiceDetailsFields.d.ts +7 -0
  12. package/dist-lib/components/InvoiceDetailsFields.d.ts.map +1 -0
  13. package/dist-lib/components/InvoiceDetailsFields.js +21 -0
  14. package/dist-lib/components/WerkbonPreviewPanel.d.ts +4 -0
  15. package/dist-lib/components/WerkbonPreviewPanel.d.ts.map +1 -0
  16. package/dist-lib/components/WerkbonPreviewPanel.js +40 -0
  17. package/dist-lib/components/settings/InstellingenPage.d.ts +5 -0
  18. package/dist-lib/components/settings/InstellingenPage.d.ts.map +1 -0
  19. package/dist-lib/components/settings/InstellingenPage.js +119 -0
  20. package/dist-lib/components/settings/InvoiceBrandingSettingsCard.d.ts +2 -0
  21. package/dist-lib/components/settings/InvoiceBrandingSettingsCard.d.ts.map +1 -0
  22. package/dist-lib/components/settings/InvoiceBrandingSettingsCard.js +64 -0
  23. package/dist-lib/components/settings/InvoiceEmailSettingsCard.d.ts +2 -0
  24. package/dist-lib/components/settings/InvoiceEmailSettingsCard.d.ts.map +1 -0
  25. package/dist-lib/components/settings/InvoiceEmailSettingsCard.js +41 -0
  26. package/dist-lib/index.d.ts +8 -0
  27. package/dist-lib/index.d.ts.map +1 -1
  28. package/dist-lib/index.js +14 -2
  29. package/dist-lib/integrations/supabase/auth-middleware.d.ts +11 -22550
  30. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  31. package/dist-lib/integrations/supabase/auth-middleware.js +3 -1
  32. package/dist-lib/integrations/supabase/client.d.ts +2 -22547
  33. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  34. package/dist-lib/integrations/supabase/client.js +4 -1
  35. package/dist-lib/integrations/supabase/client.server.d.ts +2 -22547
  36. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/client.server.js +1 -0
  38. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +6 -0
  39. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -0
  40. package/dist-lib/integrations/supabase/previewAuthStorage.js +99 -0
  41. package/dist-lib/integrations/supabase/types.d.ts +24631 -11349
  42. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  43. package/dist-lib/integrations/supabase/types.js +2 -0
  44. package/dist-lib/lib/articles.functions.d.ts +19 -0
  45. package/dist-lib/lib/articles.functions.d.ts.map +1 -0
  46. package/dist-lib/lib/articles.functions.js +91 -0
  47. package/dist-lib/lib/config.d.ts +16 -0
  48. package/dist-lib/lib/config.d.ts.map +1 -0
  49. package/dist-lib/lib/config.js +17 -0
  50. package/dist-lib/lib/facturen.d.ts +196 -0
  51. package/dist-lib/lib/facturen.d.ts.map +1 -1
  52. package/dist-lib/lib/facturen.js +441 -16
  53. package/package.json +57 -5
@@ -0,0 +1,19 @@
1
+ import { type CustomerOption, type EditLine, type InvoiceBranding, type InvoiceDetails } from "../lib/facturen";
2
+ type Props = {
3
+ branding: InvoiceBranding;
4
+ details: InvoiceDetails;
5
+ lines: EditLine[];
6
+ invoiceNumber?: string | null;
7
+ numberIsProvisional?: boolean;
8
+ logoUrl?: string | null;
9
+ printable?: boolean;
10
+ editable?: boolean;
11
+ defaultVat?: string;
12
+ onDetailsChange?: (d: InvoiceDetails) => void;
13
+ onLinesChange?: (l: EditLine[]) => void;
14
+ /** Toon klantkeuzelijst bij Klantnaam (alleen nieuwe handmatige factuur). */
15
+ onCustomerPick?: (c: CustomerOption) => void;
16
+ };
17
+ export declare const InvoiceA4Preview: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLElement>>;
18
+ export {};
19
+ //# sourceMappingURL=InvoiceA4Preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvoiceA4Preview.d.ts","sourceRoot":"","sources":["../../src/components/InvoiceA4Preview.tsx"],"names":[],"mappings":"AAKA,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;AA+CF,eAAO,MAAM,gBAAgB,+FA8K3B,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useMemo, useState } from "react";
3
+ import { DEFAULT_VAT_RATE, VAT_OPTIONS as CONFIG_VAT_OPTIONS } from "../lib/config";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "@flowselections/core";
6
+ import { ChevronDown, Package, Plus, Printer, Search, Trash2 } from "lucide-react";
7
+ import { editLineToBillable, emptyEditLine, fetchCustomers, fetchPackages, formatCents, } from "../lib/facturen";
8
+ import { listInvoiceArticles } from "../lib/articles.functions";
9
+ import { ArticlePhoto } from "./ArticlePhoto";
10
+ const VAT_OPTIONS = CONFIG_VAT_OPTIONS;
11
+ function CustomerPicker({ details, onDetailsChange, onPick }) {
12
+ const [open, setOpen] = useState(false);
13
+ const [q, setQ] = useState("");
14
+ const [items, setItems] = useState([]);
15
+ const [loading, setLoading] = useState(false);
16
+ useEffect(() => {
17
+ if (!open || items.length)
18
+ return;
19
+ setLoading(true);
20
+ fetchCustomers().then((r) => setItems(Array.isArray(r) ? r : [])).catch(() => setItems([])).finally(() => setLoading(false));
21
+ }, [open, items.length]);
22
+ const term = q.trim().toLowerCase();
23
+ const safe = Array.isArray(items) ? items : [];
24
+ const filtered = term ? safe.filter((c) => `${c.name ?? ""} ${c.customer_number ?? ""} ${c.billing_city ?? ""}`.toLowerCase().includes(term)) : safe;
25
+ 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: 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))) })] })] })] }));
26
+ }
27
+ 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) {
28
+ const safeLines = Array.isArray(lines) ? lines : [];
29
+ const listArticles = useServerFn(listInvoiceArticles);
30
+ const [articles, setArticles] = useState([]);
31
+ const [articlesLoading, setArticlesLoading] = useState(false);
32
+ const articleIds = safeLines.map((line) => line.articleId).filter((id) => Boolean(id));
33
+ useEffect(() => {
34
+ if (!editable && articleIds.length === 0)
35
+ return;
36
+ let active = true;
37
+ setArticlesLoading(true);
38
+ listArticles()
39
+ .then((result) => { if (active)
40
+ setArticles(Array.isArray(result) ? result : []); })
41
+ .catch(() => { if (active)
42
+ setArticles([]); })
43
+ .finally(() => { if (active)
44
+ setArticlesLoading(false); });
45
+ return () => { active = false; };
46
+ }, [editable, listArticles, articleIds.join(",")]);
47
+ const articleById = useMemo(() => new Map(articles.map((article) => [article.id, article])), [articles]);
48
+ const computed = safeLines.map(editLineToBillable);
49
+ const subtotal = computed.reduce((s, l) => s + (l.line_total_cents ?? 0), 0);
50
+ const vatGroups = useMemo(() => {
51
+ const m = new Map();
52
+ computed.forEach((l) => m.set(l.vat_rate, (m.get(l.vat_rate) ?? 0) + (l.line_total_cents ?? 0)));
53
+ return [...m.entries()].sort((a, b) => b[0] - a[0]).map(([rate, base]) => ({ rate, vat: Math.round((base * rate) / 100) }));
54
+ }, [computed]);
55
+ const vatTotal = vatGroups.reduce((s, g) => s + g.vat, 0);
56
+ const number = invoiceNumber || "Wordt toegekend";
57
+ const contact = [branding.address, [branding.postal_code, branding.city].filter(Boolean).join(" "), branding.email, branding.phone, branding.website].filter(Boolean);
58
+ const setD = (k) => (v) => onDetailsChange?.({ ...details, [k]: v });
59
+ const setL = (i, patch) => onLinesChange?.(safeLines.map((l, idx) => (idx === i ? { ...l, ...patch } : l)));
60
+ 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) }));
61
+ const fmtDate = (v) => (v ? new Date(`${v}T12:00:00`).toLocaleDateString("nl-NL") : "—");
62
+ 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) => {
63
+ const c = computed[i];
64
+ 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));
65
+ }) : _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, articles: articles, articlesLoading: articlesLoading, 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}}` })] }));
66
+ });
67
+ function AddLineBar({ defaultVat, articles, articlesLoading, onAdd }) {
68
+ const [packages, setPackages] = useState([]);
69
+ const [packageOpen, setPackageOpen] = useState(false);
70
+ const [packageQuery, setPackageQuery] = useState("");
71
+ const [articleOpen, setArticleOpen] = useState(false);
72
+ const [articleQuery, setArticleQuery] = useState("");
73
+ useEffect(() => {
74
+ fetchPackages().then((r) => setPackages(Array.isArray(r) ? r : [])).catch(() => setPackages([]));
75
+ }, []);
76
+ const safePackages = Array.isArray(packages) ? packages : [];
77
+ const safeArticles = Array.isArray(articles) ? articles : [];
78
+ const filteredPackages = safePackages.filter((p) => !packageQuery.trim() || [p.name, p.code, p.description].some((v) => (v ?? "").toLowerCase().includes(packageQuery.trim().toLowerCase())));
79
+ const filteredArticles = safeArticles.filter((article) => !articleQuery.trim() || [article.name, article.code, article.gtin, article.description].some((value) => (value ?? "").toLowerCase().includes(articleQuery.trim().toLowerCase())));
80
+ return (_jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2 print:hidden", children: [_jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => onAdd(emptyEditLine(defaultVat)), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Vrije regel"] }), _jsxs(Popover, { open: articleOpen, onOpenChange: setArticleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", size: "sm", variant: "outline", children: [_jsx(Search, { className: "mr-1 h-4 w-4" }), "Materiaal zoeken"] }) }), _jsxs(PopoverContent, { className: "w-[min(42rem,calc(100vw-2rem))] p-2", align: "start", children: [_jsx(Input, { autoFocus: true, placeholder: "Zoek op titel, artikelcode, EAN of omschrijving\u2026", value: articleQuery, onChange: (e) => setArticleQuery(e.target.value) }), _jsx("div", { className: "mt-2 flex max-h-[18rem] flex-wrap content-start touch-pan-y gap-1 overflow-y-auto overscroll-contain", onWheel: (event) => {
81
+ event.preventDefault();
82
+ event.stopPropagation();
83
+ event.currentTarget.scrollTop += event.deltaY;
84
+ }, children: articlesLoading ? _jsx("p", { className: "w-full p-3 text-sm text-muted-foreground", children: "Artikelen laden\u2026" }) : filteredArticles.length === 0 ? _jsx("p", { className: "w-full p-3 text-sm text-muted-foreground", children: "Geen artikelen gevonden." }) : filteredArticles.map((article) => (_jsxs(Button, { type: "button", variant: "ghost", className: "flex h-auto w-fit flex-col items-start gap-2 overflow-hidden p-2 text-left", onClick: () => {
85
+ onAdd({
86
+ description: article.description?.trim() ? `${article.name} — ${article.description.trim()}` : article.name,
87
+ quantity: "1",
88
+ unit: article.unit,
89
+ price: (article.sales_price_cents / 100).toFixed(2).replace(".", ","),
90
+ vat: defaultVat,
91
+ kind: "material",
92
+ articleId: article.id,
93
+ photoUrl: article.photo_url,
94
+ });
95
+ setArticleOpen(false);
96
+ setArticleQuery("");
97
+ }, 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: 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: () => {
98
+ const cents = Math.round(Number(p.unit_price_cents) || 0);
99
+ onAdd({
100
+ description: p.description?.trim() ? `${p.name} — ${p.description.trim()}` : p.name,
101
+ quantity: String(Number(p.default_quantity) > 0 ? Number(p.default_quantity) : 1),
102
+ unit: "pakket",
103
+ price: (cents / 100).toFixed(2).replace(".", ","),
104
+ vat: defaultVat,
105
+ kind: "package",
106
+ packageId: p.id,
107
+ });
108
+ setPackageOpen(false);
109
+ setPackageQuery("");
110
+ }, 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))) })] })] })] }));
111
+ }
112
+ function formatDueDate(invoiceDate, paymentTerm) {
113
+ if (!invoiceDate)
114
+ return "—";
115
+ const date = new Date(`${invoiceDate}T12:00:00`);
116
+ date.setDate(date.getDate() + (parseInt(paymentTerm, 10) || 0));
117
+ return date.toLocaleDateString("nl-NL");
118
+ }
@@ -0,0 +1,7 @@
1
+ import type { InvoiceDetails } from "../lib/facturen";
2
+ export declare function InvoiceDetailsFields({ value, onChange, disabled, }: {
3
+ value: InvoiceDetails;
4
+ onChange: (next: InvoiceDetails) => void;
5
+ disabled?: boolean;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=InvoiceDetailsFields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvoiceDetailsFields.d.ts","sourceRoot":"","sources":["../../src/components/InvoiceDetailsFields.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAkBtD,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,+BAgCA"}
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Input, Label } from "@flowselections/core";
3
+ const FIELDS = [
4
+ { key: "customer_name", label: "Klantnaam", group: "klant" },
5
+ { key: "customer_attention", label: "t.a.v. / p/a", group: "klant" },
6
+ { key: "customer_address", label: "Adres", group: "klant" },
7
+ { key: "customer_postal_code", label: "Postcode", group: "klant" },
8
+ { key: "customer_city", label: "Plaats", group: "klant" },
9
+ { key: "debtor_number", label: "Debiteurnummer", group: "klant" },
10
+ { key: "customer_vat_number", label: "Btw-nummer", group: "klant" },
11
+ { key: "invoice_date", label: "Factuurdatum", type: "date", group: "factuur" },
12
+ { key: "payment_term_days", label: "Betaaltermijn (dagen)", type: "number", group: "factuur" },
13
+ { key: "reference", label: "Referentie", group: "factuur" },
14
+ { key: "source_number", label: "Werkbonnummer", group: "factuur" },
15
+ { key: "work_address", label: "Werkadres", group: "factuur" },
16
+ { key: "execution_date", label: "Uitvoerdatum", type: "date", group: "factuur" },
17
+ ];
18
+ export function InvoiceDetailsFields({ value, onChange, disabled, }) {
19
+ const render = (group) => FIELDS.filter((f) => f.group === group).map((f) => (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `inv-${f.key}`, className: "text-xs", children: f.label }), _jsx(Input, { id: `inv-${f.key}`, type: f.type ?? "text", value: String(value[f.key] ?? ""), disabled: disabled, onChange: (e) => onChange({ ...value, [f.key]: e.target.value }) })] }, f.key)));
20
+ return (_jsxs("div", { className: "space-y-3 rounded-md border p-3", children: [_jsx("p", { className: "text-sm font-medium", children: "Factuurgegevens" }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium uppercase text-muted-foreground", children: "Klant / debiteur" }), render("klant")] }), _jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium uppercase text-muted-foreground", children: "Factuur" }), render("factuur")] })] })] }));
21
+ }
@@ -0,0 +1,4 @@
1
+ export declare function WerkbonPreviewPanel({ workOrderId }: {
2
+ workOrderId: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=WerkbonPreviewPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WerkbonPreviewPanel.d.ts","sourceRoot":"","sources":["../../src/components/WerkbonPreviewPanel.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,mBAAmB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,+BA0F3E"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { fetchSourceLines, fetchWorkOrderPreview, formatCents, } from "../lib/facturen";
4
+ function fmtDate(v) {
5
+ if (!v)
6
+ return "—";
7
+ const d = new Date(v);
8
+ return isNaN(d.getTime()) ? v : d.toLocaleDateString("nl-NL");
9
+ }
10
+ function Row({ label, value }) {
11
+ return (_jsxs("div", { className: "grid grid-cols-[8rem_minmax(0,1fr)] gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "min-w-0 break-words", children: value || "—" })] }));
12
+ }
13
+ export function WerkbonPreviewPanel({ workOrderId }) {
14
+ const [wo, setWo] = useState(null);
15
+ const [lines, setLines] = useState([]);
16
+ const [loading, setLoading] = useState(true);
17
+ useEffect(() => {
18
+ let active = true;
19
+ setLoading(true);
20
+ Promise.all([fetchWorkOrderPreview(workOrderId), fetchSourceLines("work_order", workOrderId)])
21
+ .then(([w, l]) => {
22
+ if (!active)
23
+ return;
24
+ setWo(w);
25
+ setLines(Array.isArray(l) ? l : []);
26
+ })
27
+ .catch(() => active && setWo(null))
28
+ .finally(() => active && setLoading(false));
29
+ return () => {
30
+ active = false;
31
+ };
32
+ }, [workOrderId]);
33
+ if (loading)
34
+ return _jsx("p", { className: "text-sm text-muted-foreground", children: "Werkbon laden\u2026" });
35
+ if (!wo)
36
+ return _jsx("p", { className: "text-sm text-muted-foreground", children: "Werkbon niet gevonden." });
37
+ const safeLines = Array.isArray(lines) ? lines : [];
38
+ const total = safeLines.reduce((s, l) => s + Math.round((Number(l.unit_price_cents) || 0) * (Number(l.quantity) || 0)), 0);
39
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Werkbon" }), _jsx("h3", { className: "text-lg font-semibold", children: wo.project_number ?? "zonder nummer" })] }), _jsxs("div", { className: "space-y-1.5 rounded-md border p-3", children: [_jsx(Row, { label: "Klant", value: wo.customer_name }), _jsx(Row, { label: "Status", value: wo.status }), _jsx(Row, { label: "Werkadres", value: [wo.work_address, wo.room_number].filter(Boolean).join(" — ") }), _jsx(Row, { label: "Uitvoerdatum", value: fmtDate(wo.scheduled_start_time) }), _jsx(Row, { label: "Referentie", value: wo.externe_referentie }), _jsx(Row, { label: "Getekend door", value: wo.signature_name })] }), wo.complaint_description && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Melding" }), _jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: wo.complaint_description })] })), wo.technician_task_description && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Opdracht" }), _jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: wo.technician_task_description })] })), wo.technician_notes && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Notities monteur" }), _jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: wo.technician_notes })] })), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Uren, materialen en pakketten" }), safeLines.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen regels." })) : (_jsxs("div", { className: "divide-y rounded-md border text-sm", children: [safeLines.map((l, i) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto] gap-2 p-2", children: [_jsxs("span", { className: "min-w-0 break-words", children: [String(l.description ?? ""), _jsxs("span", { className: "block text-xs text-muted-foreground", children: [Number(l.quantity) || 0, " ", l.unit ?? "", " \u00D7 ", formatCents(Number(l.unit_price_cents) || 0)] })] }), _jsx("span", { className: "shrink-0 tabular-nums", children: formatCents(Math.round((Number(l.unit_price_cents) || 0) * (Number(l.quantity) || 0))) })] }, i))), _jsxs("div", { className: "flex justify-between p-2 font-medium", children: [_jsx("span", { children: "Totaal excl. btw" }), _jsx("span", { className: "tabular-nums", children: formatCents(total) })] })] }))] })] }));
40
+ }
@@ -0,0 +1,5 @@
1
+ import type { FlowModule } from "@flowselections/core";
2
+ export declare function InstellingenPage({ modules }: {
3
+ modules: FlowModule[];
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=InstellingenPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InstellingenPage.d.ts","sourceRoot":"","sources":["../../../src/components/settings/InstellingenPage.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAgJvD,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,+BAmBtE"}
@@ -0,0 +1,119 @@
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, supabase, toast, } from "@flowselections/core";
4
+ import { Bell, Loader2, User } from "lucide-react";
5
+ // ─── Profielkaart ─────────────────────────────────────────────────────────────
6
+ function ProfielKaart() {
7
+ const [displayName, setDisplayName] = useState("");
8
+ const [saving, setSaving] = useState(false);
9
+ const handleUpdateProfile = async () => {
10
+ setSaving(true);
11
+ try {
12
+ const { data: { user } } = await supabase.auth.getUser();
13
+ if (!user)
14
+ throw new Error("Niet ingelogd");
15
+ const { error } = await supabase.from("profiles").update({ display_name: displayName.trim() }).eq("id", user.id);
16
+ if (error)
17
+ throw error;
18
+ toast.success("Profiel bijgewerkt");
19
+ }
20
+ catch {
21
+ toast.error("Fout bij het bijwerken van profiel");
22
+ }
23
+ finally {
24
+ setSaving(false);
25
+ }
26
+ };
27
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(User, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Profielgegevens" })] }), _jsx(CardDescription, { children: "Werk je persoonlijke gegevens bij" })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "displayName", children: "Weergavenaam" }), _jsx(Input, { id: "displayName", value: displayName, onChange: (e) => setDisplayName(e.target.value), maxLength: 100, placeholder: "Je naam" })] }), _jsxs(Button, { onClick: handleUpdateProfile, disabled: saving, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Opslaan"] })] })] }));
28
+ }
29
+ // ─── Wachtwoordkaart ──────────────────────────────────────────────────────────
30
+ function WachtwoordKaart() {
31
+ const [newPassword, setNewPassword] = useState("");
32
+ const [confirmPassword, setConfirmPassword] = useState("");
33
+ const [changingPassword, setChangingPassword] = useState(false);
34
+ const handleChangePassword = async () => {
35
+ if (newPassword.length < 6) {
36
+ toast.error("Wachtwoord moet minimaal 6 tekens bevatten");
37
+ return;
38
+ }
39
+ if (newPassword !== confirmPassword) {
40
+ toast.error("Wachtwoorden komen niet overeen");
41
+ return;
42
+ }
43
+ setChangingPassword(true);
44
+ try {
45
+ const { error } = await supabase.auth.updateUser({ password: newPassword });
46
+ if (error)
47
+ throw error;
48
+ toast.success("Wachtwoord gewijzigd");
49
+ setNewPassword("");
50
+ setConfirmPassword("");
51
+ }
52
+ catch {
53
+ toast.error("Fout bij het wijzigen van wachtwoord");
54
+ }
55
+ finally {
56
+ setChangingPassword(false);
57
+ }
58
+ };
59
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Wachtwoord wijzigen" }), _jsx(CardDescription, { children: "Kies een nieuw wachtwoord" })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "newPassword", children: "Nieuw wachtwoord" }), _jsx(Input, { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), maxLength: 128, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "confirmPassword", children: "Bevestig wachtwoord" }), _jsx(Input, { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), maxLength: 128, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), _jsxs(Button, { onClick: handleChangePassword, disabled: changingPassword, children: [changingPassword && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Wachtwoord wijzigen"] })] })] }));
60
+ }
61
+ // ─── Notificatievoorkeuren ────────────────────────────────────────────────────
62
+ function NotificatieKaart() {
63
+ const [prefs, setPrefs] = useState({ email_notifications: true });
64
+ const [loading, setLoading] = useState(true);
65
+ const [saving, setSaving] = useState(false);
66
+ useEffect(() => {
67
+ (async () => {
68
+ try {
69
+ const { data: { user } } = await supabase.auth.getUser();
70
+ if (!user)
71
+ return;
72
+ const { data } = await supabase.from("profiles").select("notification_preferences").eq("id", user.id).maybeSingle();
73
+ if (data?.notification_preferences && typeof data.notification_preferences === "object") {
74
+ setPrefs({ email_notifications: Boolean(data.notification_preferences.email_notifications ?? true) });
75
+ }
76
+ }
77
+ catch {
78
+ // negeren
79
+ }
80
+ finally {
81
+ setLoading(false);
82
+ }
83
+ })();
84
+ }, []);
85
+ const updatePref = async (value) => {
86
+ const newPrefs = { ...prefs, email_notifications: value };
87
+ setPrefs(newPrefs);
88
+ setSaving(true);
89
+ try {
90
+ const { data: { user } } = await supabase.auth.getUser();
91
+ if (!user)
92
+ throw new Error("Niet ingelogd");
93
+ const { error } = await supabase.from("profiles").update({ notification_preferences: newPrefs }).eq("id", user.id);
94
+ if (error)
95
+ throw error;
96
+ toast.success("Notificatie-instelling opgeslagen");
97
+ }
98
+ catch {
99
+ setPrefs(prefs);
100
+ toast.error("Fout bij opslaan");
101
+ }
102
+ finally {
103
+ setSaving(false);
104
+ }
105
+ };
106
+ if (loading) {
107
+ return _jsx(Card, { children: _jsx(CardContent, { className: "flex items-center justify-center py-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) }) });
108
+ }
109
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Bell, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Notificatievoorkeuren" })] }), _jsx(CardDescription, { children: "Beheer je notificatie-instellingen" })] }), _jsx(CardContent, { className: "space-y-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: "E-mailnotificaties" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Ontvang meldingen per e-mail" })] }), _jsx(Switch, { checked: prefs.email_notifications, onCheckedChange: (v) => void updatePref(v), disabled: saving })] }) })] }));
110
+ }
111
+ // ─── Instellingenpagina ───────────────────────────────────────────────────────
112
+ // Volgorde: Profiel | Wachtwoord, daarna de modulekaarten (Factuurhuisstijl naast
113
+ // Factuur e-mail) en Notificatievoorkeuren onderaan over de volle breedte.
114
+ export function InstellingenPage({ modules }) {
115
+ const moduleCards = modules
116
+ .flatMap((m) => m.settingsCards)
117
+ .sort((a, b) => a.order - b.order);
118
+ return (_jsx("main", { className: "p-6", children: _jsxs("div", { className: "grid gap-6 lg:grid-cols-2", children: [_jsx(ProfielKaart, {}), _jsx(WachtwoordKaart, {}), moduleCards.map((card, i) => (_jsx(card.component, {}, i))), _jsx("div", { className: "lg:col-span-2", children: _jsx(NotificatieKaart, {}) })] }) }));
119
+ }
@@ -0,0 +1,2 @@
1
+ export declare function InvoiceBrandingSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=InvoiceBrandingSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvoiceBrandingSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/InvoiceBrandingSettingsCard.tsx"],"names":[],"mappings":"AAiBA,wBAAgB,2BAA2B,gCAkD1C"}
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { STORAGE_BUCKETS } from "../../lib/config";
4
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Input, Label, supabase, toast } from "@flowselections/core";
5
+ import { Building2, ImagePlus, Loader2, Trash2 } from "lucide-react";
6
+ import { emptyInvoiceBranding, fetchInvoiceBranding, getInvoiceLogoUrl, saveInvoiceBranding } from "../../lib/facturen";
7
+ const fields = [
8
+ { key: "company_name", label: "Bedrijfsnaam" }, { key: "address", label: "Adres" },
9
+ { key: "postal_code", label: "Postcode" }, { key: "city", label: "Plaats" },
10
+ { key: "chamber_of_commerce_number", label: "KvK-nummer" },
11
+ { key: "vat_identification_number", label: "Btw-identificatienummer" },
12
+ { key: "email", label: "E-mailadres", type: "email", optional: true },
13
+ { key: "phone", label: "Telefoonnummer", optional: true }, { key: "iban", label: "IBAN" },
14
+ { key: "website", label: "Website", optional: true },
15
+ { key: "payment_term_days", label: "Standaard betaaltermijn (dagen)", type: "number" },
16
+ ];
17
+ export function InvoiceBrandingSettingsCard() {
18
+ const [value, setValue] = useState(emptyInvoiceBranding());
19
+ const [logoUrl, setLogoUrl] = useState(null);
20
+ const [loading, setLoading] = useState(true);
21
+ const [saving, setSaving] = useState(false);
22
+ useEffect(() => {
23
+ fetchInvoiceBranding().then(async (row) => {
24
+ setValue(row);
25
+ setLogoUrl(await getInvoiceLogoUrl(row.logo_path));
26
+ }).catch(() => toast.error("Factuurhuisstijl kon niet worden geladen")).finally(() => setLoading(false));
27
+ }, []);
28
+ const uploadLogo = async (file) => {
29
+ if (!file)
30
+ return;
31
+ if (!["image/png", "image/jpeg", "image/webp", "image/svg+xml"].includes(file.type) || file.size > 2 * 1024 * 1024) {
32
+ toast.error("Gebruik PNG, JPEG, WebP of SVG van maximaal 2 MB");
33
+ return;
34
+ }
35
+ const extension = file.name.split(".").pop()?.toLowerCase() || "png";
36
+ const path = `facturen-installatietechniek/${crypto.randomUUID()}.${extension}`;
37
+ const { error } = await supabase.storage.from(STORAGE_BUCKETS.logos).upload(path, file, { contentType: file.type, upsert: false });
38
+ if (error) {
39
+ toast.error("Logo uploaden mislukt");
40
+ return;
41
+ }
42
+ setValue((current) => ({ ...current, logo_path: path }));
43
+ setLogoUrl(await getInvoiceLogoUrl(path));
44
+ };
45
+ const save = async () => {
46
+ const letters = value.company_name.toUpperCase().replace(/[^A-Z]/g, "");
47
+ if (letters.length < 2) {
48
+ toast.error("De bedrijfsnaam moet minimaal twee letters bevatten");
49
+ return;
50
+ }
51
+ setSaving(true);
52
+ try {
53
+ setValue(await saveInvoiceBranding(value));
54
+ toast.success("Factuurhuisstijl opgeslagen");
55
+ }
56
+ catch {
57
+ toast.error("Factuurhuisstijl opslaan mislukt");
58
+ }
59
+ finally {
60
+ setSaving(false);
61
+ }
62
+ };
63
+ return _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Building2, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Factuurhuisstijl" })] }), _jsx(CardDescription, { children: "Bedrijfsgegevens en kleuren voor nieuwe A4-facturen" })] }), _jsx(CardContent, { className: "space-y-5", children: loading ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bedrijfslogo" }), _jsxs("div", { className: "flex min-h-28 items-center gap-4 rounded-md border p-3", children: [_jsx("div", { className: "flex h-20 w-36 items-center justify-center bg-muted", children: logoUrl ? _jsx("img", { src: logoUrl, alt: "Bedrijfslogo", className: "max-h-20 max-w-36 object-contain" }) : _jsx(ImagePlus, { className: "h-7 w-7 text-muted-foreground" }) }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", variant: "outline", asChild: true, children: _jsxs("label", { children: ["Afbeelding kiezen", _jsx("input", { className: "sr-only", type: "file", accept: "image/png,image/jpeg,image/webp,image/svg+xml", onChange: (e) => void uploadLogo(e.target.files?.[0]) })] }) }), value.logo_path ? _jsx(Button, { type: "button", variant: "ghost", size: "icon", title: "Logo verwijderen", onClick: () => { setValue((v) => ({ ...v, logo_path: null })); setLogoUrl(null); }, children: _jsx(Trash2, { className: "h-4 w-4" }) }) : null] })] })] }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: fields.map((field) => _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `branding-${field.key}`, children: [field.label, field.optional ? " (optioneel)" : ""] }), _jsx(Input, { id: `branding-${field.key}`, type: field.type || "text", min: field.type === "number" ? 1 : undefined, max: field.type === "number" ? 365 : undefined, value: String(value[field.key] ?? ""), onChange: (e) => setValue((v) => ({ ...v, [field.key]: field.type === "number" ? Number(e.target.value) : e.target.value })) }), field.key === "company_name" ? _jsxs("label", { htmlFor: "branding-show-company-name", className: "flex cursor-pointer items-center gap-2 pt-0.5 text-xs text-muted-foreground", children: [_jsx(Checkbox, { id: "branding-show-company-name", checked: value.show_company_name !== false, onCheckedChange: (c) => setValue((v) => ({ ...v, show_company_name: c === true })) }), "Bedrijfsnaam naast het logo tonen in de bovenbalk"] }) : null] }, field.key)) }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: ["primary_color", "secondary_color"].map((key, index) => _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `branding-${key}`, children: ["Bedrijfskleur ", index + 1] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { className: "h-10 w-14 p-1", id: `branding-${key}`, type: "color", value: value[key], onChange: (e) => setValue((v) => ({ ...v, [key]: e.target.value.toUpperCase() })) }), _jsx(Input, { value: value[key], onChange: (e) => setValue((v) => ({ ...v, [key]: e.target.value.toUpperCase() })) })] })] }, key)) }), _jsxs("div", { className: "overflow-hidden rounded-md border", children: [_jsx("div", { className: "h-12 px-4 py-3 text-sm font-semibold text-primary-foreground", style: { backgroundColor: value.primary_color }, children: value.show_company_name !== false ? (value.company_name || "Bedrijfsnaam") : "Logo" }), _jsx("div", { className: "h-8 px-4 py-2 text-xs text-primary-foreground", style: { backgroundColor: value.secondary_color }, children: "KvK \u00B7 Btw-id \u00B7 IBAN" })] }), _jsxs(Button, { onClick: save, disabled: saving, children: [saving ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, "Opslaan"] })] }) })] });
64
+ }
@@ -0,0 +1,2 @@
1
+ export declare function InvoiceEmailSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=InvoiceEmailSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvoiceEmailSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/InvoiceEmailSettingsCard.tsx"],"names":[],"mappings":"AAKA,wBAAgB,wBAAwB,gCAuCvC"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast } from "@flowselections/core";
4
+ import { Mail } from "lucide-react";
5
+ import { EMAIL_PURPOSE_OPTIONS, fetchInvoiceEmailPurpose, fetchSenderForPurpose, saveInvoiceEmailPurpose } from "../../lib/facturen";
6
+ export function InvoiceEmailSettingsCard() {
7
+ const [purpose, setPurpose] = useState("");
8
+ const [sender, setSender] = useState(null);
9
+ const [loading, setLoading] = useState(true);
10
+ const loadSender = async (p) => {
11
+ try {
12
+ setSender(await fetchSenderForPurpose(p || null));
13
+ }
14
+ catch {
15
+ setSender(null);
16
+ }
17
+ };
18
+ useEffect(() => {
19
+ fetchInvoiceEmailPurpose()
20
+ .then(async (p) => { setPurpose(p ?? ""); await loadSender(p ?? ""); })
21
+ .catch(() => toast.error("E-mailinstelling kon niet worden geladen"))
22
+ .finally(() => setLoading(false));
23
+ }, []);
24
+ const change = async (v) => {
25
+ const prev = purpose;
26
+ setPurpose(v);
27
+ try {
28
+ await saveInvoiceEmailPurpose(v);
29
+ await loadSender(v);
30
+ toast.success("Doeleinde opgeslagen");
31
+ }
32
+ catch (e) {
33
+ setPurpose(prev);
34
+ toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
35
+ }
36
+ };
37
+ const options = Array.isArray(EMAIL_PURPOSE_OPTIONS) ? EMAIL_PURPOSE_OPTIONS : [];
38
+ return _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Factuur e-mail" })] }), _jsx(CardDescription, { children: "Kies het e-maildoeleinde waarmee facturen worden verstuurd. Welk adres daarbij hoort stel je in bij E-mailkoppeling." })] }), _jsx(CardContent, { className: "space-y-3", children: loading ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" }) : _jsxs(_Fragment, { children: [_jsxs(Select, { value: purpose, onValueChange: (v) => void change(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een doeleinde" }) }), _jsx(SelectContent, { children: options.map((o) => _jsx(SelectItem, { value: o.key, children: o.label }, o.key)) })] }), !purpose ? _jsx("p", { className: "text-sm text-destructive", children: "Nog geen doeleinde gekozen \u2014 facturen kunnen niet worden verzonden." })
39
+ : sender ? _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Verzonden vanaf: ", _jsx("span", { className: "font-medium text-foreground", children: sender.account_email })] })
40
+ : _jsx("p", { className: "text-sm text-destructive", children: "Aan dit doeleinde is geen mailbox gekoppeld voor verzenden." })] }) })] });
41
+ }
@@ -1,7 +1,15 @@
1
1
  import type { FlowModule } from "@flowselections/core";
2
+ import { InvoiceEmailSettingsCard } from "./components/settings/InvoiceEmailSettingsCard";
3
+ export { InvoiceEmailSettingsCard };
2
4
  export * from './_core-safelist';
3
5
  export { FacturenForInstallatieTechniek } from './components/FacturenForInstallatieTechniek';
4
6
  export { FactuurDetailPage } from './components/FactuurDetailPage';
5
7
  export { TemplateModuleLogo } from './components/TemplateModuleLogo';
6
8
  export declare const facturenInstallatietechniekModule: FlowModule;
9
+ export { InvoiceDetailsFields } from "./components/InvoiceDetailsFields";
10
+ export { WerkbonPreviewPanel } from './components/WerkbonPreviewPanel';
11
+ export { InvoiceA4Preview } from "./components/InvoiceA4Preview";
12
+ export { ArticlePhoto } from "./components/ArticlePhoto";
13
+ export { InvoiceBrandingSettingsCard } from "./components/settings/InvoiceBrandingSettingsCard";
14
+ export { InstellingenPage } from "./components/settings/InstellingenPage";
7
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,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,UAY/C,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,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,UAe/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
@@ -2,6 +2,9 @@
2
2
  // src/index.ts — MODULE CONTRACT
3
3
  // ============================================================================
4
4
  import { FileText } from "lucide-react";
5
+ import { InvoiceBrandingSettingsCard } from "./components/settings/InvoiceBrandingSettingsCard";
6
+ import { InvoiceEmailSettingsCard } from "./components/settings/InvoiceEmailSettingsCard";
7
+ export { InvoiceEmailSettingsCard };
5
8
  export * from './_core-safelist';
6
9
  export { FacturenForInstallatieTechniek } from './components/FacturenForInstallatieTechniek';
7
10
  export { FactuurDetailPage } from './components/FactuurDetailPage';
@@ -9,11 +12,20 @@ export { TemplateModuleLogo } from './components/TemplateModuleLogo';
9
12
  export const facturenInstallatietechniekModule = {
10
13
  id: "facturen-installatietechniek",
11
14
  name: "Facturen Installatietechniek",
12
- version: "1.0.0",
15
+ version: "1.3.3", // houd gelijk aan package.json (zie src/lib/module-version.test.ts)
13
16
  nav: {
14
17
  label: "Facturen",
15
18
  href: "/facturen-installatietechniek",
16
19
  icon: FileText,
17
20
  },
18
- settingsCards: [],
21
+ settingsCards: [
22
+ { component: InvoiceBrandingSettingsCard, order: 20 },
23
+ { component: InvoiceEmailSettingsCard, order: 21 },
24
+ ],
19
25
  };
26
+ export { InvoiceDetailsFields } from "./components/InvoiceDetailsFields";
27
+ export { WerkbonPreviewPanel } from './components/WerkbonPreviewPanel';
28
+ export { InvoiceA4Preview } from "./components/InvoiceA4Preview";
29
+ export { ArticlePhoto } from "./components/ArticlePhoto";
30
+ export { InvoiceBrandingSettingsCard } from "./components/settings/InvoiceBrandingSettingsCard";
31
+ export { InstellingenPage } from "./components/settings/InstellingenPage";