@flowselections/facturen-installatie-techniek 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/FacturenForInstallatieTechniek.d.ts +2 -0
- package/dist-lib/components/FacturenForInstallatieTechniek.d.ts.map +1 -0
- package/dist-lib/components/FacturenForInstallatieTechniek.js +161 -0
- package/dist-lib/components/FactuurDetailPage.d.ts +2 -0
- package/dist-lib/components/FactuurDetailPage.d.ts.map +1 -0
- package/dist-lib/components/FactuurDetailPage.js +51 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
- package/dist-lib/components/TemplateModuleLogo.js +4 -0
- package/dist-lib/components/TemplateModulePage.d.ts +2 -0
- package/dist-lib/components/TemplateModulePage.d.ts.map +1 -0
- package/dist-lib/components/TemplateModulePage.js +44 -0
- package/dist-lib/components/TemplatePage.d.ts +2 -0
- package/dist-lib/components/TemplatePage.d.ts.map +1 -0
- package/dist-lib/components/TemplatePage.js +4 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
- package/dist-lib/hooks/useMijnModuleData.js +49 -0
- package/dist-lib/index.d.ts +7 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +19 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +22553 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
- package/dist-lib/integrations/supabase/client.d.ts +22549 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +13 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +22549 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +30 -0
- package/dist-lib/integrations/supabase/types.d.ts +23165 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +92 -0
- package/dist-lib/lib/facturen.d.ts +77 -0
- package/dist-lib/lib/facturen.d.ts.map +1 -0
- package/dist-lib/lib/facturen.js +102 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +92 -0
- package/public/logo.png +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Forceert Tailwind classes die uit @flowselections/core komen maar niet
|
|
2
|
+
// voorkomen in de eigen broncode van deze module.
|
|
3
|
+
// Zonder dit bestand worden deze classes weggeoptimaliseerd op de
|
|
4
|
+
// Lovable productie build.
|
|
5
|
+
const _safelist = [
|
|
6
|
+
// Layout — Sidebar en shell structuur
|
|
7
|
+
'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
|
|
8
|
+
'items-start', 'items-center', 'justify-between', 'border-t',
|
|
9
|
+
'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
|
|
10
|
+
// Grid — InstellingenPage
|
|
11
|
+
'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
|
|
12
|
+
// Toggle/Switch — aan/uit zichtbaarheid
|
|
13
|
+
'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
|
|
14
|
+
];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FacturenForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/FacturenForInstallatieTechniek.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,8BAA8B,gCAoZ7C"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Link } from "@tanstack/react-router";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
5
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
6
|
+
import { createInvoiceFromSource, euroToCents, fetchInvoices, fetchSourceLines, formatCents, listSources, SOURCE_LABELS, STATUS_LABELS, } from "../lib/facturen";
|
|
7
|
+
const emptyLine = () => ({ description: "", quantity: "1", unit: "stuk", price: "" });
|
|
8
|
+
function statusVariant(status) {
|
|
9
|
+
if (status === "betaald")
|
|
10
|
+
return "default";
|
|
11
|
+
if (status === "vervallen")
|
|
12
|
+
return "destructive";
|
|
13
|
+
if (status === "concept")
|
|
14
|
+
return "outline";
|
|
15
|
+
return "secondary";
|
|
16
|
+
}
|
|
17
|
+
export function FacturenForInstallatieTechniek() {
|
|
18
|
+
const [invoices, setInvoices] = useState([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [statusFilter, setStatusFilter] = useState("alle");
|
|
21
|
+
const [sourceFilter, setSourceFilter] = useState("alle");
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [saving, setSaving] = useState(false);
|
|
24
|
+
// bron-tab
|
|
25
|
+
const [sourceType, setSourceType] = useState("work_order");
|
|
26
|
+
const [sources, setSources] = useState([]);
|
|
27
|
+
const [sourcesLoading, setSourcesLoading] = useState(false);
|
|
28
|
+
const [selectedSource, setSelectedSource] = useState("");
|
|
29
|
+
// handmatig-tab
|
|
30
|
+
const [customerName, setCustomerName] = useState("");
|
|
31
|
+
const [invoiceDate, setInvoiceDate] = useState(() => new Date().toISOString().slice(0, 10));
|
|
32
|
+
const [vatRate, setVatRate] = useState("21");
|
|
33
|
+
const [lines, setLines] = useState([emptyLine()]);
|
|
34
|
+
const load = useCallback(async () => {
|
|
35
|
+
setLoading(true);
|
|
36
|
+
try {
|
|
37
|
+
setInvoices(await fetchInvoices());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
toast.error("Fout bij het laden van facturen");
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void load();
|
|
48
|
+
}, [load]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open)
|
|
51
|
+
return;
|
|
52
|
+
let active = true;
|
|
53
|
+
setSourcesLoading(true);
|
|
54
|
+
setSelectedSource("");
|
|
55
|
+
listSources(sourceType)
|
|
56
|
+
.then((rows) => {
|
|
57
|
+
if (active)
|
|
58
|
+
setSources(rows);
|
|
59
|
+
})
|
|
60
|
+
.catch(() => toast.error("Bronnen konden niet worden geladen"))
|
|
61
|
+
.finally(() => {
|
|
62
|
+
if (active)
|
|
63
|
+
setSourcesLoading(false);
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
active = false;
|
|
67
|
+
};
|
|
68
|
+
}, [open, sourceType]);
|
|
69
|
+
const filtered = useMemo(() => invoices.filter((i) => (statusFilter === "alle" || i.status === statusFilter) &&
|
|
70
|
+
(sourceFilter === "alle" || i.source_type === sourceFilter)), [invoices, statusFilter, sourceFilter]);
|
|
71
|
+
const kpi = useMemo(() => {
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const openCents = invoices
|
|
74
|
+
.filter((i) => i.status === "verzonden" || i.status === "vervallen")
|
|
75
|
+
.reduce((sum, i) => sum + (i.total_cents || 0), 0);
|
|
76
|
+
const overdue = invoices.filter((i) => i.status === "vervallen").length;
|
|
77
|
+
const monthCents = invoices
|
|
78
|
+
.filter((i) => {
|
|
79
|
+
const d = new Date(i.invoice_date);
|
|
80
|
+
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
|
|
81
|
+
})
|
|
82
|
+
.reduce((sum, i) => sum + (i.total_cents || 0), 0);
|
|
83
|
+
return { openCents, overdue, monthCents };
|
|
84
|
+
}, [invoices]);
|
|
85
|
+
const createFromSource = async () => {
|
|
86
|
+
const source = sources.find((s) => s.id === selectedSource);
|
|
87
|
+
if (!source) {
|
|
88
|
+
toast.error("Kies eerst een bron");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setSaving(true);
|
|
92
|
+
try {
|
|
93
|
+
const sourceLines = await fetchSourceLines(sourceType, source.id);
|
|
94
|
+
if (sourceLines.length === 0) {
|
|
95
|
+
toast.error("Deze bron heeft geen factureerbare regels");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const result = await createInvoiceFromSource({
|
|
99
|
+
sourceType,
|
|
100
|
+
sourceId: source.id,
|
|
101
|
+
sourceNumber: source.number,
|
|
102
|
+
customerId: source.customer_id,
|
|
103
|
+
customerName: source.customer_name,
|
|
104
|
+
lines: sourceLines,
|
|
105
|
+
});
|
|
106
|
+
toast.success(result.reused ? `Bestaand concept ${result.invoice_number}` : `Factuur ${result.invoice_number} aangemaakt`);
|
|
107
|
+
setOpen(false);
|
|
108
|
+
await load();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
toast.error("Factuur aanmaken mislukt");
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
setSaving(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const createManual = async () => {
|
|
118
|
+
const payloadLines = lines
|
|
119
|
+
.filter((l) => l.description.trim() !== "")
|
|
120
|
+
.map((l, index) => {
|
|
121
|
+
const quantity = parseFloat(l.quantity.replace(",", ".")) || 0;
|
|
122
|
+
const unitPrice = euroToCents(l.price);
|
|
123
|
+
return {
|
|
124
|
+
description: l.description.trim(),
|
|
125
|
+
kind: "other",
|
|
126
|
+
quantity,
|
|
127
|
+
unit: l.unit || "stuk",
|
|
128
|
+
unit_price_cents: unitPrice,
|
|
129
|
+
line_total_cents: Math.round(quantity * unitPrice),
|
|
130
|
+
sort_order: index + 1,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
if (!customerName.trim() || payloadLines.length === 0) {
|
|
134
|
+
toast.error("Vul een klantnaam en minimaal één regel in");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setSaving(true);
|
|
138
|
+
try {
|
|
139
|
+
const result = await createInvoiceFromSource({
|
|
140
|
+
sourceType: "manual",
|
|
141
|
+
sourceId: null,
|
|
142
|
+
customerName: customerName.trim(),
|
|
143
|
+
vatRate: parseFloat(vatRate.replace(",", ".")) || 21,
|
|
144
|
+
invoiceDate,
|
|
145
|
+
lines: payloadLines,
|
|
146
|
+
});
|
|
147
|
+
toast.success(`Factuur ${result.invoice_number} aangemaakt`);
|
|
148
|
+
setCustomerName("");
|
|
149
|
+
setLines([emptyLine()]);
|
|
150
|
+
setOpen(false);
|
|
151
|
+
await load();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
toast.error("Factuur aanmaken mislukt");
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
setSaving(false);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Openstaand" }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: formatCents(kpi.openCents) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Vervallen" }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: kpi.overdue })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Deze maand" }) }), _jsx(CardContent, { className: "text-2xl font-semibold", children: formatCents(kpi.monthCents) })] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs(Select, { value: statusFilter, onValueChange: setStatusFilter, children: [_jsx(SelectTrigger, { className: "w-44", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle statussen" }), Object.entries(STATUS_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsxs(Select, { value: sourceFilter, onValueChange: setSourceFilter, children: [_jsx(SelectTrigger, { className: "w-44", children: _jsx(SelectValue, { placeholder: "Bron" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "alle", children: "Alle bronnen" }), Object.entries(SOURCE_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: () => setOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Nieuwe factuur"] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Facturen" }) }), _jsx(CardContent, { children: loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden\u2026" })) : filtered.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen facturen gevonden." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Nummer" }), _jsx(TableHead, { children: "Klant" }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { className: "text-right", children: "Totaal" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: filtered.map((invoice) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsx(Link, { to: "/facturen-installatietechniek/detail/$id", params: { id: invoice.id }, className: "underline-offset-2 hover:underline", children: invoice.invoice_number ?? "—" }) }), _jsx(TableCell, { children: invoice.customer_name ?? "—" }), _jsx(TableCell, { children: _jsxs(Badge, { variant: "outline", children: [SOURCE_LABELS[invoice.source_type] ?? invoice.source_type, invoice.source_number ? ` ${invoice.source_number}` : ""] }) }), _jsx(TableCell, { children: new Date(invoice.invoice_date).toLocaleDateString("nl-NL") }), _jsx(TableCell, { className: "text-right", children: formatCents(invoice.total_cents) }), _jsx(TableCell, { children: _jsx(Badge, { variant: statusVariant(invoice.status), children: STATUS_LABELS[invoice.status] ?? invoice.status }) })] }, invoice.id))) })] })) })] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Nieuwe factuur" }) }), _jsxs(Tabs, { defaultValue: "bron", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "bron", children: "Uit bron" }), _jsx(TabsTrigger, { value: "handmatig", children: "Handmatig" })] }), _jsxs(TabsContent, { value: "bron", className: "space-y-4 pt-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Brontype" }), _jsxs(Select, { value: sourceType, onValueChange: (v) => setSourceType(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "work_order", children: "Werkbon" }), _jsx(SelectItem, { value: "quotation", children: "Offerte" })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bron" }), sourcesLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Bronnen laden\u2026" })) : sources.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen factureerbare bronnen gevonden." })) : (_jsxs(Select, { value: selectedSource, onValueChange: setSelectedSource, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een bron" }) }), _jsx(SelectContent, { children: sources.map((s) => (_jsx(SelectItem, { value: s.id, children: (s.number ?? "zonder nummer") + (s.customer_name ? ` — ${s.customer_name}` : "") }, s.id))) })] }))] }), _jsx(Button, { onClick: createFromSource, disabled: saving || !selectedSource, className: "w-full", children: saving ? "Bezig…" : "Factuur aanmaken" })] }), _jsxs(TabsContent, { value: "handmatig", className: "space-y-4 pt-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs("div", { className: "space-y-2 sm:col-span-1", children: [_jsx(Label, { htmlFor: "klant", children: "Klantnaam" }), _jsx(Input, { id: "klant", value: customerName, onChange: (e) => setCustomerName(e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "datum", children: "Factuurdatum" }), _jsx(Input, { id: "datum", type: "date", value: invoiceDate, onChange: (e) => setInvoiceDate(e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "btw", children: "Btw %" }), _jsx(Input, { id: "btw", value: vatRate, onChange: (e) => setVatRate(e.target.value) })] })] }), _jsxs("div", { className: "space-y-3", children: [_jsx(Label, { children: "Regels" }), lines.map((line, index) => (_jsxs("div", { className: "grid grid-cols-12 items-end gap-2", children: [_jsx(Input, { className: "col-span-5", placeholder: "Omschrijving", value: line.description, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, description: e.target.value } : l))) }), _jsx(Input, { className: "col-span-2", placeholder: "Aantal", value: line.quantity, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, quantity: e.target.value } : l))) }), _jsx(Input, { className: "col-span-2", placeholder: "Eenheid", value: line.unit, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, unit: e.target.value } : l))) }), _jsx(Input, { className: "col-span-2", placeholder: "Prijs", value: line.price, onChange: (e) => setLines((prev) => prev.map((l, i) => (i === index ? { ...l, price: e.target.value } : l))) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "col-span-1", onClick: () => setLines((prev) => (prev.length === 1 ? [emptyLine()] : prev.filter((_, i) => i !== index))), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setLines((prev) => [...prev, emptyLine()]), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Regel toevoegen"] })] }), _jsx(Button, { onClick: createManual, disabled: saving, className: "w-full", children: saving ? "Bezig…" : "Factuur aanmaken" })] })] })] }) })] }));
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FactuurDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/FactuurDetailPage.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,iBAAiB,gCAsKhC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { Link, useParams } from "@tanstack/react-router";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast, } from "@flowselections/core";
|
|
5
|
+
import { ArrowLeft } from "lucide-react";
|
|
6
|
+
import { fetchInvoice, fetchInvoiceItems, formatCents, KIND_LABELS, SOURCE_LABELS, STATUS_LABELS, updateInvoiceStatus, } from "../lib/facturen";
|
|
7
|
+
export function FactuurDetailPage() {
|
|
8
|
+
const { id } = useParams({ from: "/_authenticated/facturen-installatietechniek/detail/$id" });
|
|
9
|
+
const [invoice, setInvoice] = useState(null);
|
|
10
|
+
const [items, setItems] = useState([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [saving, setSaving] = useState(false);
|
|
13
|
+
const load = useCallback(async () => {
|
|
14
|
+
setLoading(true);
|
|
15
|
+
try {
|
|
16
|
+
const [inv, rows] = await Promise.all([fetchInvoice(id), fetchInvoiceItems(id)]);
|
|
17
|
+
setInvoice(inv);
|
|
18
|
+
setItems(rows);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
toast.error("Factuur kon niet worden geladen");
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
}, [id]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
void load();
|
|
29
|
+
}, [load]);
|
|
30
|
+
const changeStatus = async (status) => {
|
|
31
|
+
setSaving(true);
|
|
32
|
+
try {
|
|
33
|
+
await updateInvoiceStatus(id, status);
|
|
34
|
+
toast.success("Status bijgewerkt");
|
|
35
|
+
await load();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
toast.error("Status bijwerken mislukt");
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setSaving(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
if (loading) {
|
|
45
|
+
return _jsx("main", { className: "p-6 text-sm text-muted-foreground", children: "Laden\u2026" });
|
|
46
|
+
}
|
|
47
|
+
if (!invoice) {
|
|
48
|
+
return (_jsxs("main", { className: "space-y-4 p-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Factuur niet gevonden." }), _jsx(Button, { asChild: true, variant: "outline", children: _jsx(Link, { to: "/facturen-installatietechniek", children: "Terug naar overzicht" }) })] }));
|
|
49
|
+
}
|
|
50
|
+
return (_jsxs("main", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-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}` : ""] }), _jsx("div", { className: "ml-auto w-48", children: _jsxs(Select, { value: invoice.status, onValueChange: (v) => changeStatus(v), disabled: saving, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.entries(STATUS_LABELS).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] }) })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Klant" }) }), _jsx(CardContent, { className: "text-base", children: invoice.customer_name ?? "—" })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Factuurdatum" }) }), _jsx(CardContent, { className: "text-base", children: new Date(invoice.invoice_date).toLocaleDateString("nl-NL") })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: "Totaal incl. btw" }) }), _jsx(CardContent, { className: "text-base font-semibold", children: formatCents(invoice.total_cents) })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Regels" }) }), _jsxs(CardContent, { className: "space-y-4", children: [items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Deze factuur heeft geen regels." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Omschrijving" }), _jsx(TableHead, { children: "Soort" }), _jsx(TableHead, { className: "text-right", children: "Aantal" }), _jsx(TableHead, { children: "Eenheid" }), _jsx(TableHead, { className: "text-right", children: "Stukprijs" }), _jsx(TableHead, { className: "text-right", children: "Totaal" })] }) }), _jsx(TableBody, { children: items.map((item) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: item.description }), _jsx(TableCell, { children: KIND_LABELS[item.kind] ?? item.kind }), _jsx(TableCell, { className: "text-right", children: Number(item.quantity) }), _jsx(TableCell, { children: item.unit }), _jsx(TableCell, { className: "text-right", children: formatCents(item.unit_price_cents) }), _jsx(TableCell, { className: "text-right", children: formatCents(item.total_cents) })] }, item.id))) })] })), _jsxs("div", { className: "ml-auto w-64 space-y-1 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Subtotaal" }), _jsx("span", { children: formatCents(invoice.subtotal_cents) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Btw ", Number(invoice.vat_rate), "%"] }), _jsx("span", { children: formatCents(invoice.vat_cents) })] }), _jsxs("div", { className: "flex justify-between font-semibold", children: [_jsx("span", { children: "Totaal" }), _jsx("span", { children: formatCents(invoice.total_cents) })] })] })] })] }), invoice.note ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Notitie" }) }), _jsx(CardContent, { className: "text-sm", children: invoice.note })] })) : null] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateModuleLogo.d.ts","sourceRoot":"","sources":["../../src/components/TemplateModuleLogo.tsx"],"names":[],"mappings":"AAQA,UAAU,uBAAuB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE,uBAAuB,+BAwBxE"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function TemplateModuleLogo({ className }) {
|
|
3
|
+
return (_jsxs("svg", { viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className, "aria-hidden": "true", children: [_jsx("rect", { width: "40", height: "40", rx: "8", fill: "currentColor", opacity: "0.1" }), _jsx("text", { x: "50%", y: "50%", dominantBaseline: "middle", textAnchor: "middle", fontSize: "18", fontWeight: "600", fill: "currentColor", children: "M" })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateModulePage.d.ts","sourceRoot":"","sources":["../../src/components/TemplateModulePage.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,kBAAkB,gCAmFjC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// src/components/TemplateModulePage.tsx — EXAMPLE PAGE COMPONENT
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Pagina-componenten horen in src/components/, NIET alleen in src/routes/.
|
|
6
|
+
// src/routes/ wordt NIET naar npm gepubliceerd (zie tsconfig.build.json exclude).
|
|
7
|
+
//
|
|
8
|
+
// GEEN <Header /> hier — die zit in _authenticated.tsx via staticData.title
|
|
9
|
+
// ============================================================================
|
|
10
|
+
import { useState, useEffect } from "react";
|
|
11
|
+
import { useAuth, supabase, Button, Card, CardContent, CardHeader, CardTitle, toast, } from "@flowselections/core";
|
|
12
|
+
import { TemplateModuleLogo } from './TemplateModuleLogo';
|
|
13
|
+
export function TemplateModulePage() {
|
|
14
|
+
// Auth is guaranteed — this user is already logged in
|
|
15
|
+
const { user, isAdmin } = useAuth();
|
|
16
|
+
// Example state — replace with your own
|
|
17
|
+
const [items, setItems] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
loadData();
|
|
21
|
+
}, []);
|
|
22
|
+
const loadData = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
// Example query — replace "products" with your table
|
|
26
|
+
const { data, error } = await supabase
|
|
27
|
+
.from("products")
|
|
28
|
+
.select("*")
|
|
29
|
+
.limit(10);
|
|
30
|
+
if (error)
|
|
31
|
+
throw error;
|
|
32
|
+
// ✅ Altijd Array.isArray bewaken op server-function data
|
|
33
|
+
// const safeItems = Array.isArray(data?.items) ? data.items : [];
|
|
34
|
+
setItems(Array.isArray(data) ? data : []);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
toast.error("Fout bij het laden van gegevens");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [isAdmin && (_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Ingelogd als admin: ", user?.email] })), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Overzicht" }) }), _jsxs(CardContent, { children: [loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen gegevens gevonden." })) : (_jsxs("p", { className: "text-sm", children: [items.length, " items geladen."] })), _jsx(Button, { onClick: loadData, className: "mt-4", disabled: loading, children: "Vernieuwen" })] })] }), _jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(TemplateModuleLogo, { className: "w-6 h-6 text-primary" }), _jsx("span", { children: "SVG-component (voorkeur voor vector-logo's)" })] })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplatePage.d.ts","sourceRoot":"","sources":["../../src/components/TemplatePage.tsx"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,gCAS3B"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function TemplatePage() {
|
|
3
|
+
return (_jsxs("div", { className: "p-6", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Module pagina" }), _jsx("p", { className: "text-muted-foreground mt-2", children: "Vervang dit component met de pagina's van jouw module." })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MijnModuleSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/MijnModuleSettingsCard.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,sBAAsB,gCA+CrC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// src/components/settings/MijnModuleSettingsCard.tsx — EXAMPLE SETTINGS CARD
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// This is an example of a settings card for your module.
|
|
6
|
+
// It appears on the main settings page when registered in src/index.ts.
|
|
7
|
+
//
|
|
8
|
+
// RULES:
|
|
9
|
+
// - Must return a <Card> as its root element
|
|
10
|
+
// - Handles its own data fetching — no props are passed by the core
|
|
11
|
+
// - Use the same Card primitives from @flowselections/core
|
|
12
|
+
//
|
|
13
|
+
// WHAT TO DO:
|
|
14
|
+
// - Rename this file to match your card's purpose
|
|
15
|
+
// - Add your real settings UI
|
|
16
|
+
// - Register it in src/index.ts under settingsCards
|
|
17
|
+
// - Delete this file entirely if your module has no settings
|
|
18
|
+
// ============================================================================
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Button, Input, Label, toast, } from "@flowselections/core";
|
|
21
|
+
import { Settings2 } from "lucide-react";
|
|
22
|
+
export function MijnModuleSettingsCard() {
|
|
23
|
+
// Replace with your real settings state
|
|
24
|
+
const [setting, setSetting] = useState("");
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
const handleSave = async () => {
|
|
27
|
+
setSaving(true);
|
|
28
|
+
try {
|
|
29
|
+
// Example: save to a table — replace with your real query
|
|
30
|
+
// const { error } = await supabase.from("my_settings").upsert({ ... });
|
|
31
|
+
// if (error) throw error;
|
|
32
|
+
toast.success("Instellingen opgeslagen");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
toast.error("Fout bij het opslaan");
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setSaving(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Settings2, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Module Instellingen" }), " "] }), _jsx(CardDescription, { children: "Beheer de instellingen voor deze module " })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "setting", children: "Instelling" }), _jsx(Input, { id: "setting", value: setting, onChange: (e) => setSetting(e.target.value), placeholder: "Waarde..." })] }), _jsx(Button, { onClick: handleSave, disabled: saving, children: saving ? "Opslaan..." : "Opslaan" })] })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface MijnItem {
|
|
2
|
+
id: string;
|
|
3
|
+
created_at: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare function useMijnModuleData(): {
|
|
7
|
+
items: MijnItem[];
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
reload: () => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=useMijnModuleData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMijnModuleData.d.ts","sourceRoot":"","sources":["../../src/hooks/useMijnModuleData.ts"],"names":[],"mappings":"AAkBA,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,iBAAiB;;;;;EAkChC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/hooks/useMijnModuleData.ts — EXAMPLE DATA HOOK
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Put your data fetching logic in hooks like this, not directly in components.
|
|
5
|
+
// This keeps your pages clean and makes it easy to reuse data across pages.
|
|
6
|
+
//
|
|
7
|
+
// WHAT TO DO:
|
|
8
|
+
// - Rename to match your data (e.g. useProducts, useOrders, useSuppliers)
|
|
9
|
+
// - Replace the example query with your real Supabase query
|
|
10
|
+
// - Replace MijnItem with your actual table's row type
|
|
11
|
+
// - Import and use in your page components
|
|
12
|
+
// - Delete this file if you prefer a different pattern
|
|
13
|
+
// ============================================================================
|
|
14
|
+
import { useState, useEffect, useCallback } from "react";
|
|
15
|
+
import { supabase, toast } from "@flowselections/core";
|
|
16
|
+
export function useMijnModuleData() {
|
|
17
|
+
const [items, setItems] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const load = useCallback(async () => {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
const { data, error } = await supabase
|
|
25
|
+
.from("products") // ← CHANGE THIS to your table
|
|
26
|
+
.select("*")
|
|
27
|
+
.order("created_at", { ascending: false });
|
|
28
|
+
if (error)
|
|
29
|
+
throw error;
|
|
30
|
+
setItems(data ?? []);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
setError("Fout bij het laden van gegevens");
|
|
34
|
+
toast.error("Fout bij het laden van gegevens");
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
load();
|
|
42
|
+
}, [load]);
|
|
43
|
+
return {
|
|
44
|
+
items,
|
|
45
|
+
loading,
|
|
46
|
+
error,
|
|
47
|
+
reload: load,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowModule } from "@flowselections/core";
|
|
2
|
+
export * from './_core-safelist';
|
|
3
|
+
export { FacturenForInstallatieTechniek } from './components/FacturenForInstallatieTechniek';
|
|
4
|
+
export { FactuurDetailPage } from './components/FactuurDetailPage';
|
|
5
|
+
export { TemplateModuleLogo } from './components/TemplateModuleLogo';
|
|
6
|
+
export declare const facturenInstallatietechniekModule: FlowModule;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/index.ts — MODULE CONTRACT
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { FileText } from "lucide-react";
|
|
5
|
+
export * from './_core-safelist';
|
|
6
|
+
export { FacturenForInstallatieTechniek } from './components/FacturenForInstallatieTechniek';
|
|
7
|
+
export { FactuurDetailPage } from './components/FactuurDetailPage';
|
|
8
|
+
export { TemplateModuleLogo } from './components/TemplateModuleLogo';
|
|
9
|
+
export const facturenInstallatietechniekModule = {
|
|
10
|
+
id: "facturen-installatietechniek",
|
|
11
|
+
name: "Facturen Installatietechniek",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
nav: {
|
|
14
|
+
label: "Facturen",
|
|
15
|
+
href: "/facturen-installatietechniek",
|
|
16
|
+
icon: FileText,
|
|
17
|
+
},
|
|
18
|
+
settingsCards: [],
|
|
19
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,mHAQ9B,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// This file is automatically generated. Do not edit it directly.
|
|
2
|
+
import { createMiddleware } from '@tanstack/react-start';
|
|
3
|
+
import { supabase } from './client';
|
|
4
|
+
// Must be registered as a global `functionMiddleware` in `src/start.ts`; otherwise
|
|
5
|
+
// the browser never attaches the bearer token to serverFn RPCs.
|
|
6
|
+
export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client(async ({ next }) => {
|
|
7
|
+
const { data } = await supabase.auth.getSession();
|
|
8
|
+
const token = data.session?.access_token;
|
|
9
|
+
return next({
|
|
10
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
11
|
+
});
|
|
12
|
+
});
|