import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { FilterX } from 'lucide-react' import { apiFetch } from '@/lib/api' import { useTheme } from '@/hooks/useTheme' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import type { Expense } from '@/lib/types' type PeriodKey = 'month' | '30d' | '90d' | 'year' | 'all' type ExpenseType = '' | 'fuel' | 'parts' | 'tools' | 'other' const PERIOD_OPTIONS: { key: PeriodKey; label: string }[] = [ { key: 'month', label: 'Mês atual' }, { key: '30d', label: '30 dias' }, { key: '90d', label: '90 dias' }, { key: 'year', label: '12 meses' }, { key: 'all', label: 'Todo o histórico' }, ] const TYPE_LABEL: Record, string> = { fuel: 'Combustível', parts: 'Peças', tools: 'Ferramentas', other: 'Outros', } function periodStart(period: PeriodKey) { const now = new Date() if (period === 'all') return null const start = new Date(now) if (period === 'month') { start.setDate(1) start.setHours(0, 0, 0, 0) return start } if (period === '30d') start.setDate(now.getDate() - 30) if (period === '90d') start.setDate(now.getDate() - 90) if (period === 'year') start.setMonth(now.getMonth() - 12) return start } function currency(v: number) { return new Intl.NumberFormat('pt-PT', { style: 'currency', currency: 'EUR' }).format(v) } function monthKey(d: Date) { return `${d.getFullYear()}-${d.getMonth()}` } export default function ExpenseReportsPage() { const { theme } = useTheme('ui_theme', 'dark') const isLight = theme === 'light' const [period, setPeriod] = useState('90d') const [typeFilter, setTypeFilter] = useState('') const expensesQ = useQuery({ queryKey: ['expenses', 'reports'], queryFn: () => apiFetch('/expenses'), }) const expenses = expensesQ.data ?? [] const filtered = useMemo(() => { const start = periodStart(period) return expenses.filter((e) => { if (typeFilter && e.type !== typeFilter) return false if (start && new Date(e.date) < start) return false return true }) }, [expenses, period, typeFilter]) const hasFilters = period !== '90d' || !!typeFilter const kpis = useMemo(() => { const total = filtered.reduce((sum, e) => sum + e.amount, 0) const count = filtered.length const avg = count > 0 ? total / count : 0 const maxExpense = filtered.reduce((mx, e) => (e.amount > mx.amount ? e : mx), { amount: 0, type: 'other' as Expense['type'] }) const byType = { fuel: 0, parts: 0, tools: 0, other: 0, } for (const e of filtered) byType[e.type] += e.amount const topType = (Object.entries(byType) as [keyof typeof byType, number][]) .sort((a, b) => b[1] - a[1])[0] return { total, count, avg, maxValue: maxExpense.amount, maxType: maxExpense.type, topType: topType?.[0] ?? 'other', topTypeAmount: topType?.[1] ?? 0, } }, [filtered]) const byTypeRows = useMemo(() => { const totals = { fuel: 0, parts: 0, tools: 0, other: 0, } for (const e of filtered) totals[e.type] += e.amount return (Object.entries(totals) as [keyof typeof totals, number][]) .map(([type, total]) => ({ type, total, pct: kpis.total > 0 ? (total / kpis.total) * 100 : 0 })) .sort((a, b) => b.total - a.total) }, [filtered, kpis.total]) const monthly = useMemo(() => { const now = new Date() const buckets = Array.from({ length: 6 }).map((_, i) => { const d = new Date(now.getFullYear(), now.getMonth() - i, 1) return { key: monthKey(d), label: new Intl.DateTimeFormat('pt-PT', { month: 'short' }).format(d), total: 0, count: 0, } }).reverse() const index = new Map(buckets.map((b, i) => [b.key, i])) for (const e of filtered) { const idx = index.get(monthKey(new Date(e.date))) if (idx === undefined) continue buckets[idx].total += e.amount buckets[idx].count += 1 } return buckets }, [filtered]) const maxMonthly = Math.max(1, ...monthly.map((m) => m.total)) const topExpenses = [...filtered] .sort((a, b) => b.amount - a.amount) .slice(0, 7) return (

Relatórios Financeiros

Relatório de Despesas

Acompanhamento de custos por período e categoria para suportar decisões operacionais.

Ações rápidas
{expenses.length} total {filtered.length} no filtro atual
{[ ['Total de despesas', currency(kpis.total)], ['Registos', String(kpis.count)], ['Média por registo', currency(kpis.avg)], ['Maior despesa', currency(kpis.maxValue)], ].map(([label, value]) => (

{label}

{value}

))}
{expensesQ.isLoading && (

A calcular relatório de despesas...

)}

Distribuição por Categoria

Categoria com maior peso: {TYPE_LABEL[kpis.topType as keyof typeof TYPE_LABEL]} ({currency(kpis.topTypeAmount)})

{byTypeRows.map((row) => (
{TYPE_LABEL[row.type]} {currency(row.total)} ({row.pct.toFixed(1)}%)
))}

Evolução Mensal (Despesas)

{monthly.map((m) => (
{m.label} {m.count} reg. | {currency(m.total)}
))}

Top Despesas do Período

{topExpenses.length === 0 ? (

Sem despesas para o filtro atual.

) : (
{topExpenses.map((e) => ( ))}
Data Categoria Descrição Valor
{new Intl.DateTimeFormat('pt-PT').format(new Date(e.date))} {TYPE_LABEL[e.type]} {e.description || '—'} {currency(e.amount)}
)}
) }