'use client' import React, { useMemo, useState } from 'react' import PageLayout from '../../components/PageLayout' import { useRouter } from 'next/navigation' import { useVatRates } from './hooks/getTaxes' import { useAdminInvoices, type AdminInvoice } from './hooks/getInvoices' import useAuthStore from '../../store/authStore' import InvoiceDetailModal from './components/InvoiceDetailModal' export default function FinanceManagementPage() { const router = useRouter() const accessToken = useAuthStore(s => s.accessToken) const { rates, loading: vatLoading, error: vatError } = useVatRates() const [timeframe, setTimeframe] = useState<'7d' | '30d' | '90d' | 'ytd'>('30d') const [billFilter, setBillFilter] = useState({ query: '', status: 'all', from: '', to: '' }) const [diagLoading, setDiagLoading] = useState(false) const [diagError, setDiagError] = useState('') const [diagData, setDiagData] = useState(null) const [selectedInvoice, setSelectedInvoice] = useState(null) const [detailModalOpen, setDetailModalOpen] = useState(false) const [emailDialogOpen, setEmailDialogOpen] = useState(false) const [reportEmail, setReportEmail] = useState('') const [sendingReport, setSendingReport] = useState(false) const [reportMsg, setReportMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // NEW: fetch invoices from backend const { invoices, loading: invLoading, error: invError, reload, } = useAdminInvoices({ status: billFilter.status !== 'all' ? billFilter.status : undefined, limit: 200, offset: 0, }) // NEW: totals from backend invoices const totals = useMemo(() => { const now = new Date() const inRange = (d: Date) => { const diff = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24) if (timeframe === '7d') return diff <= 7 if (timeframe === '30d') return diff <= 30 if (timeframe === '90d') return diff <= 90 return true } const range = invoices.filter(inv => { const dStr = inv.issued_at ?? inv.created_at if (!dStr) return false const d = new Date(dStr) return inRange(d) }) const totalAll = invoices.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0) const totalRange = range.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0) return { totalAll, totalRange } }, [invoices, timeframe]) // NEW: filtered rows for table const filteredBills = useMemo(() => { const q = billFilter.query.trim().toLowerCase() const from = billFilter.from ? new Date(billFilter.from) : null const to = billFilter.to ? new Date(billFilter.to) : null return invoices.filter(inv => { const byQuery = !q || String(inv.invoice_number ?? inv.id).toLowerCase().includes(q) || String(inv.buyer_name ?? '').toLowerCase().includes(q) const issued = inv.issued_at ? new Date(inv.issued_at) : (inv.created_at ? new Date(inv.created_at) : null) const byFrom = from ? (issued ? issued >= from : false) : true const byTo = to ? (issued ? issued <= to : false) : true return byQuery && byFrom && byTo }) }, [invoices, billFilter]) const exportBills = (format: 'csv' | 'pdf') => { console.log('[export]', format, { filters: billFilter, invoices: filteredBills }) alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} invoices`) } const runPoolCheck = async (invoiceId: string | number) => { setDiagLoading(true) setDiagError('') setDiagData(null) try { const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' const url = `${base}/api/admin/pools/inflow-diagnostics?invoiceId=${encodeURIComponent(String(invoiceId))}` const res = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, }) const body = await res.json().catch(() => ({})) if (!res.ok || body?.success === false) { setDiagError(body?.message || `Check failed (${res.status})`) return } setDiagData(body?.data || null) } catch (e: any) { setDiagError(e?.message || 'Network error') } finally { setDiagLoading(false) } } const exportInvoice = (inv: AdminInvoice) => { const pretty = JSON.stringify(inv, null, 2) const blob = new Blob([pretty], { type: 'application/json;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `invoice-${inv.invoice_number || inv.id}.json` document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } const [pdfLoading, setPdfLoading] = useState(null) const viewInvoicePdf = async (inv: AdminInvoice) => { setPdfLoading(inv.id) try { const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' const res = await fetch(`${base}/api/invoices/${inv.id}/pdf`, { method: 'GET', credentials: 'include', headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, }) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body?.message || `Failed to load PDF (${res.status})`) } const blob = await res.blob() const blobUrl = URL.createObjectURL(blob) window.open(blobUrl, '_blank', 'noopener,noreferrer') } catch (e: any) { setReportMsg({ type: 'error', text: e?.message || 'Failed to load invoice PDF.' }) } finally { setPdfLoading(null) } } const sendEmailReport = async () => { if (!reportEmail.trim()) return setReportMsg(null) setSendingReport(true) try { const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' const res = await fetch(`${base}/api/admin/invoices/email-report`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify({ email: reportEmail.trim(), from: billFilter.from || undefined, to: billFilter.to || undefined, }), }) const body = await res.json().catch(() => ({})) if (!res.ok || body?.success === false) { throw new Error(body?.message || `Request failed (${res.status})`) } setReportMsg({ type: 'success', text: `Report sent to ${reportEmail.trim()} (${body.data?.sentCount ?? 0} paid invoice(s)).` }) setEmailDialogOpen(false) setReportEmail('') } catch (e: any) { setReportMsg({ type: 'error', text: e?.message || 'Failed to send email report.' }) } finally { setSendingReport(false) } } return (

Finance Management

Overview of taxes, revenue, and invoices.

{/* Stats */}
Total revenue (all time)
€{totals.totalAll.toFixed(2)}
Revenue (range)
€{totals.totalRange.toFixed(2)}
Invoices (range)
{filteredBills.length}
Timeframe
{/* VAT summary */}

Manage VAT rates

Live data from backend; edit on a separate page.

{vatLoading && 'Loading VAT rates...'} {vatError && {vatError}} {!vatLoading && !vatError && ( <>Active countries: {rates.length} • Examples: {rates.slice(0, 5).map(r => r.country_code).join(', ')} )}
{/* Bills list & filters */}

Invoices

setBillFilter(f => ({ ...f, query: e.target.value }))} className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent" /> setBillFilter(f => ({ ...f, from: e.target.value }))} className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent" /> setBillFilter(f => ({ ...f, to: e.target.value }))} className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent" />
{reportMsg && (
{reportMsg.text}
)} {invError && (
{invError}
)} {(diagLoading || diagError || diagData) && (
{diagLoading &&
Checking pool inflow...
} {!diagLoading && diagError &&
{diagError}
} {!diagLoading && !diagError && diagData && (
Pool inflow diagnostic for invoice #{diagData.invoice_id ?? '—'}
Status: {diagData.ok ? 'OK' : 'Blocked'} • Reason: {diagData.reason}
{diagData.ok && (
Abonement: {diagData.abonement_id} • Will book: {diagData.will_book_count} • Already booked: {diagData.already_booked_count}
)} {Array.isArray(diagData.candidates) && diagData.candidates.length > 0 && (
{diagData.candidates.map((c: any) => ( ))}
Pool Coffee Capsules Amount (gross) Booked
{c.pool_name} #{c.coffee_table_id} {c.capsules_count} €{Number(c.amount_gross ?? c.amount_net ?? 0).toFixed(2)} {c.already_booked ? 'yes' : 'no'}
)}
)}
)} {invLoading ? ( <> ) : filteredBills.length === 0 ? ( ) : ( filteredBills.map(inv => ( )) )}
Invoice Customer Issued Due Date Amount Status Actions
Keine Rechnungen gefunden.
{inv.invoice_number ?? inv.id} {inv.buyer_name ?? '—'} {inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'} {(() => { if (!inv.due_at) return const due = new Date(inv.due_at) const now = new Date() const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) let cls = 'bg-green-100 text-green-700' // plenty of time if (inv.status === 'paid') cls = 'bg-green-100 text-green-700' else if (diffDays < 0) cls = 'bg-red-100 text-red-700' else if (diffDays <= 3) cls = 'bg-red-100 text-red-700' else if (diffDays <= 7) cls = 'bg-amber-100 text-amber-700' return ( {due.toLocaleDateString()} ) })()} €{Number(inv.total_gross ?? 0).toFixed(2)}{' '} {inv.currency ?? 'EUR'} {inv.status}
{selectedInvoice && ( { setDetailModalOpen(false); setTimeout(() => setSelectedInvoice(null), 200) }} onStatusChanged={reload} onRunPoolCheck={(id) => { setDetailModalOpen(false); runPoolCheck(id) }} onExport={(inv) => exportInvoice(inv)} /> )} {/* Email Report Dialog */} {emailDialogOpen && (

Send Email Report

Only paid invoices will be included in the report, regardless of the status filter. {(billFilter.from || billFilter.to) && ( The current date range filter ({billFilter.from || '…'} – {billFilter.to || '…'}) will be applied. )}
setReportEmail(e.target.value)} placeholder="email@example.com" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent" autoFocus onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }} />
)}
) }