dev #21

Merged
Seazn merged 35 commits from dev into main 2026-05-21 17:34:44 +00:00
Showing only changes of commit 9a6a02a9e8 - Show all commits

View File

@ -121,6 +121,16 @@ export default function FinanceManagementPage() {
} }
const [pdfLoading, setPdfLoading] = useState<string | number | null>(null) const [pdfLoading, setPdfLoading] = useState<string | number | null>(null)
const [uploadModalOpen, setUploadModalOpen] = useState(false)
const [uploadForm, setUploadForm] = useState({
buyer_name: '', buyer_email: '', buyer_street: '', buyer_postal_code: '',
buyer_city: '', buyer_country: '', currency: 'EUR',
total_gross: '', vat_rate: '20',
status: 'issued', issued_at: '', due_at: '',
})
const [uploadFile, setUploadFile] = useState<File | null>(null)
const [uploading, setUploading] = useState(false)
const [uploadError, setUploadError] = useState<string | null>(null)
const viewInvoicePdf = async (inv: AdminInvoice) => { const viewInvoicePdf = async (inv: AdminInvoice) => {
setPdfLoading(inv.id) setPdfLoading(inv.id)
@ -147,6 +157,52 @@ export default function FinanceManagementPage() {
} }
} }
const submitUploadInvoice = async () => {
if (!uploadForm.total_gross || isNaN(Number(uploadForm.total_gross))) {
setUploadError('Total gross (Bruttobetrag) is required.')
return
}
setUploadError(null)
setUploading(true)
try {
const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
const gross = parseFloat(uploadForm.total_gross)
const rate = parseFloat(uploadForm.vat_rate) || 0
const net = rate > 0 ? +(gross / (1 + rate / 100)).toFixed(2) : gross
const tax = +(gross - net).toFixed(2)
const fd = new FormData()
const { total_gross, vat_rate, ...rest } = uploadForm
Object.entries(rest).forEach(([k, v]) => { if (v !== '') fd.append(k, v) })
fd.append('total_gross', gross.toFixed(2))
fd.append('total_net', net.toFixed(2))
fd.append('total_tax', tax.toFixed(2))
fd.append('vat_rate', String(rate))
if (uploadFile) fd.append('pdf', uploadFile)
const res = await fetch(`${base}/api/admin/invoices`, {
method: 'POST',
credentials: 'include',
headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) },
body: fd,
})
const body = await res.json().catch(() => ({}))
if (!res.ok || body?.success === false) throw new Error(body?.message || `Upload failed (${res.status})`)
setUploadModalOpen(false)
setUploadForm({
buyer_name: '', buyer_email: '', buyer_street: '', buyer_postal_code: '',
buyer_city: '', buyer_country: '', currency: 'EUR',
total_gross: '', vat_rate: '20',
status: 'issued', issued_at: '', due_at: '',
})
setUploadFile(null)
reload()
setReportMsg({ type: 'success', text: `Invoice ${body.data?.invoice_number ?? ''} created successfully.` })
} catch (e: any) {
setUploadError(e?.message || 'Upload failed.')
} finally {
setUploading(false)
}
}
const sendEmailReport = async () => { const sendEmailReport = async () => {
if (!reportEmail.trim()) return if (!reportEmail.trim()) return
setReportMsg(null) setReportMsg(null)
@ -246,6 +302,7 @@ export default function FinanceManagementPage() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold text-[#1C2B4A]">Invoices</h2> <h2 className="text-lg font-semibold text-[#1C2B4A]">Invoices</h2>
<div className="flex flex-wrap gap-2 text-sm"> <div className="flex flex-wrap gap-2 text-sm">
<button onClick={() => { setUploadError(null); setUploadModalOpen(true) }} className="rounded-lg bg-[#1C2B4A] px-3 py-2 text-white font-medium hover:bg-[#1C2B4A]/90">Upload Invoice</button>
<button onClick={() => { setReportMsg(null); setEmailDialogOpen(true) }} className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 font-medium hover:bg-blue-100">Send Email Report</button> <button onClick={() => { setReportMsg(null); setEmailDialogOpen(true) }} className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 font-medium hover:bg-blue-100">Send Email Report</button>
<button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV</button> <button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV</button>
<button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF</button> <button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF</button>
@ -444,6 +501,110 @@ export default function FinanceManagementPage() {
/> />
)} )}
{/* Upload Invoice Modal */}
{uploadModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="w-full max-w-2xl rounded-2xl bg-white p-6 shadow-2xl overflow-y-auto max-h-[90vh]">
<h3 className="text-lg font-semibold text-[#1C2B4A] mb-4">Upload Invoice</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Customer Name</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_name} onChange={e => setUploadForm(f => ({ ...f, buyer_name: e.target.value }))} placeholder="Max Mustermann" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Customer Email</label>
<input type="email" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_email} onChange={e => setUploadForm(f => ({ ...f, buyer_email: e.target.value }))} placeholder="kunde@example.com" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Street</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_street} onChange={e => setUploadForm(f => ({ ...f, buyer_street: e.target.value }))} placeholder="Musterstraße 1" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Postal Code</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_postal_code} onChange={e => setUploadForm(f => ({ ...f, buyer_postal_code: e.target.value }))} placeholder="8010" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">City</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_city} onChange={e => setUploadForm(f => ({ ...f, buyer_city: e.target.value }))} placeholder="Graz" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Country</label>
<input className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.buyer_country} onChange={e => setUploadForm(f => ({ ...f, buyer_country: e.target.value }))} placeholder="Austria" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Total Gross / Brutto <span className="text-red-500">*</span></label>
<input type="number" step="0.01" min="0" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.total_gross} onChange={e => setUploadForm(f => ({ ...f, total_gross: e.target.value }))} placeholder="0.00" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">MwSt. / VAT Rate (%)</label>
<input type="number" step="0.01" min="0" max="100" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.vat_rate} onChange={e => setUploadForm(f => ({ ...f, vat_rate: e.target.value }))} placeholder="20" />
</div>
{(() => {
const gross = parseFloat(uploadForm.total_gross) || 0
const rate = parseFloat(uploadForm.vat_rate) || 0
const net = rate > 0 ? +(gross / (1 + rate / 100)).toFixed(2) : gross
const tax = +(gross - net).toFixed(2)
return (
<div className="sm:col-span-2 grid grid-cols-2 gap-3">
<div className="rounded-lg bg-gray-50 border border-gray-100 px-3 py-2">
<div className="text-xs text-gray-500 mb-0.5">Netto (calculated)</div>
<div className="font-semibold text-gray-800">{uploadForm.currency} {net.toFixed(2)}</div>
</div>
<div className="rounded-lg bg-gray-50 border border-gray-100 px-3 py-2">
<div className="text-xs text-gray-500 mb-0.5">MwSt. (calculated)</div>
<div className="font-semibold text-gray-800">{uploadForm.currency} {tax.toFixed(2)}</div>
</div>
</div>
)
})()}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Currency</label>
<select className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.currency} onChange={e => setUploadForm(f => ({ ...f, currency: e.target.value }))}>
<option value="EUR">EUR</option>
<option value="CHF">CHF</option>
<option value="USD">USD</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Status</label>
<select className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.status} onChange={e => setUploadForm(f => ({ ...f, status: e.target.value }))}>
<option value="issued">Issued</option>
<option value="paid">Paid</option>
<option value="draft">Draft</option>
<option value="overdue">Overdue</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Issue Date</label>
<input type="date" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.issued_at} onChange={e => setUploadForm(f => ({ ...f, issued_at: e.target.value }))} />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Due Date</label>
<input type="date" className="w-full rounded-lg border border-gray-200 px-3 py-2" value={uploadForm.due_at} onChange={e => setUploadForm(f => ({ ...f, due_at: e.target.value }))} />
</div>
<div className="sm:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1">PDF File</label>
<input
type="file" accept="application/pdf"
className="w-full text-sm text-gray-700 file:mr-3 file:rounded-lg file:border-0 file:bg-blue-50 file:px-3 file:py-2 file:text-blue-900 file:font-medium hover:file:bg-blue-100"
onChange={e => setUploadFile(e.target.files?.[0] ?? null)}
/>
{uploadFile && <p className="mt-1 text-xs text-gray-500">{uploadFile.name}</p>}
</div>
</div>
{uploadError && (
<div className="mt-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{uploadError}</div>
)}
<div className="mt-5 flex items-center justify-end gap-2">
<button onClick={() => { setUploadModalOpen(false); setUploadError(null) }} disabled={uploading} className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-60">Cancel</button>
<button onClick={submitUploadInvoice} disabled={uploading} className="rounded-lg bg-[#1C2B4A] px-4 py-2 text-sm font-semibold text-white shadow hover:bg-[#1C2B4A]/90 disabled:opacity-60 disabled:cursor-not-allowed">
{uploading ? 'Uploading…' : 'Create Invoice'}
</button>
</div>
</div>
</div>
)}
{/* Email Report Dialog */} {/* Email Report Dialog */}
{emailDialogOpen && ( {emailDialogOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">