profit-planet-frontend/src/app/admin/finance-management/page.tsx
2025-12-15 16:59:16 +01:00

247 lines
12 KiB
TypeScript

'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 } from './hooks/getInvoices'
export default function FinanceManagementPage() {
const router = useRouter()
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: '' })
// 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`)
}
return (
<PageLayout>
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
<header className="rounded-2xl bg-white border border-blue-100 shadow-lg px-8 py-8 flex flex-col gap-2">
<h1 className="text-3xl font-extrabold text-blue-900">Finance Management</h1>
<p className="text-sm text-blue-700">Overview of taxes, revenue, and invoices.</p>
</header>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
<div className="text-xs text-gray-500 mb-1">Total revenue (all time)</div>
<div className="text-2xl font-semibold text-[#1C2B4A]">{totals.totalAll.toFixed(2)}</div>
</div>
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
<div className="text-xs text-gray-500 mb-1">Revenue (range)</div>
<div className="text-2xl font-semibold text-[#1C2B4A]">{totals.totalRange.toFixed(2)}</div>
</div>
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
<div className="text-xs text-gray-500 mb-1">Invoices (range)</div>
<div className="text-2xl font-semibold text-[#1C2B4A]">{filteredBills.length}</div>
</div>
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
<div className="text-xs text-gray-500 mb-1">Timeframe</div>
<select
value={timeframe}
onChange={e => setTimeframe(e.target.value as any)}
className="mt-2 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent"
>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="ytd">YTD</option>
</select>
</div>
</div>
{/* VAT summary */}
<section className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 space-y-3">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[#1C2B4A]">Manage VAT rates</h2>
<p className="text-xs text-gray-600">Live data from backend; edit on a separate page.</p>
</div>
<button
onClick={() => router.push('/admin/finance-management/vat-edit')}
className="rounded-lg bg-[#1C2B4A] px-4 py-2 text-sm font-semibold text-white shadow hover:bg-[#1C2B4A]/90"
>
Edit VAT
</button>
</div>
<div className="text-sm text-gray-700">
{vatLoading && 'Loading VAT rates...'}
{vatError && <span className="text-red-600">{vatError}</span>}
{!vatLoading && !vatError && (
<>Active countries: {rates.length} Examples: {rates.slice(0, 5).map(r => r.country_code).join(', ')}</>
)}
</div>
</section>
{/* Bills list & filters */}
<section className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 space-y-4">
<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>
<div className="flex flex-wrap gap-2 text-sm">
<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={reload} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Reload</button>
</div>
</div>
<div className="grid gap-3 md:grid-cols-4 text-sm">
<input
placeholder="Search (invoice no., customer)"
value={billFilter.query}
onChange={e => 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"
/>
<select
value={billFilter.status}
onChange={e => setBillFilter(f => ({ ...f, status: e.target.value }))}
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
>
<option value="all">Status: All</option>
<option value="draft">Draft</option>
<option value="issued">Issued</option>
<option value="paid">Paid</option>
<option value="overdue">Overdue</option>
<option value="canceled">Canceled</option>
</select>
<input
type="date"
value={billFilter.from}
onChange={e => 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"
/>
<input
type="date"
value={billFilter.to}
onChange={e => 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"
/>
</div>
<div className="overflow-x-auto">
{invError && (
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 mb-3">
{invError}
</div>
)}
<table className="min-w-full text-sm">
<thead>
<tr className="bg-blue-50 text-left text-blue-900">
<th className="px-3 py-2 font-semibold">Invoice</th>
<th className="px-3 py-2 font-semibold">Customer</th>
<th className="px-3 py-2 font-semibold">Issued</th>
<th className="px-3 py-2 font-semibold">Amount</th>
<th className="px-3 py-2 font-semibold">Status</th>
<th className="px-3 py-2 font-semibold">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{invLoading ? (
<>
<tr><td colSpan={6} className="px-3 py-3"><div className="h-4 w-40 bg-gray-200 animate-pulse rounded" /></td></tr>
<tr><td colSpan={6} className="px-3 py-3"><div className="h-4 w-3/4 bg-gray-200 animate-pulse rounded" /></td></tr>
</>
) : filteredBills.length === 0 ? (
<tr>
<td colSpan={6} className="px-3 py-4 text-center text-gray-500">
Keine Rechnungen gefunden.
</td>
</tr>
) : (
filteredBills.map(inv => (
<tr key={inv.id} className="border-b last:border-0">
<td className="px-3 py-2">{inv.invoice_number ?? inv.id}</td>
<td className="px-3 py-2">{inv.buyer_name ?? '—'}</td>
<td className="px-3 py-2">{inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'}</td>
<td className="px-3 py-2">
{Number(inv.total_gross ?? 0).toFixed(2)}{' '}
<span className="text-xs text-gray-500">{inv.currency ?? 'EUR'}</span>
</td>
<td className="px-3 py-2">
<span
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
inv.status === 'paid'
? 'bg-green-100 text-green-700'
: inv.status === 'issued'
? 'bg-indigo-100 text-indigo-700'
: inv.status === 'draft'
? 'bg-gray-100 text-gray-700'
: inv.status === 'overdue'
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
}`}
>
{inv.status}
</span>
</td>
<td className="px-3 py-2 space-x-2">
<button className="text-xs rounded border px-2 py-1 hover:bg-gray-50">View</button>
<button className="text-xs rounded border px-2 py-1 hover:bg-gray-50">Export</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
</div>
</PageLayout>
)
}