import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { reportService, categoryService } from '../services/api';
import useFormatters from '../hooks/useFormatters';
import BalanceProjectionChart from '../components/dashboard/BalanceProjectionChart';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
ArcElement,
Title,
Tooltip,
Legend,
Filler,
} from 'chart.js';
import { Bar, Line, Doughnut, Pie } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
ArcElement,
Title,
Tooltip,
Legend,
Filler
);
const Reports = () => {
const { t } = useTranslation();
const { currency, formatDate } = useFormatters();
const [activeTab, setActiveTab] = useState('summary');
const [loading, setLoading] = useState(true);
const [year, setYear] = useState(new Date().getFullYear());
const [months, setMonths] = useState(12);
// Data states
const [summary, setSummary] = useState(null);
const [categoryData, setCategoryData] = useState(null);
const [evolutionData, setEvolutionData] = useState(null);
const [dayOfWeekData, setDayOfWeekData] = useState(null);
const [topExpenses, setTopExpenses] = useState(null);
const [projection, setProjection] = useState(null);
const [comparison, setComparison] = useState(null);
const [costCenterData, setCostCenterData] = useState(null);
const [recurringData, setRecurringData] = useState(null);
const [liabilitiesData, setLiabilitiesData] = useState(null);
const [futureData, setFutureData] = useState(null);
const [overdueData, setOverdueData] = useState(null);
// Load data based on active tab
const loadData = useCallback(async () => {
setLoading(true);
try {
switch (activeTab) {
case 'summary':
const summaryRes = await reportService.getSummary({ year });
setSummary(summaryRes);
break;
case 'category':
const catRes = await reportService.getByCategory({ type: 'debit' });
setCategoryData(catRes);
break;
case 'evolution':
const evoRes = await reportService.getMonthlyEvolution({ months });
setEvolutionData(evoRes);
break;
case 'dayOfWeek':
const dowRes = await reportService.getByDayOfWeek({ months: 6 });
setDayOfWeekData(dowRes);
break;
case 'topExpenses':
const topRes = await reportService.getTopExpenses({ limit: 20 });
setTopExpenses(topRes);
break;
case 'projection':
const projRes = await reportService.getProjection();
setProjection(projRes);
break;
case 'comparison':
const compRes = await reportService.comparePeriods();
setComparison(compRes);
break;
case 'costCenter':
const ccRes = await reportService.getByCostCenter();
setCostCenterData(ccRes);
break;
case 'recurring':
const recRes = await reportService.getRecurringReport();
setRecurringData(recRes);
break;
case 'liabilities':
const liabRes = await reportService.getLiabilities();
setLiabilitiesData(liabRes);
break;
case 'future':
const futRes = await reportService.getFutureTransactions({ days: 30 });
setFutureData(futRes);
break;
case 'overdue':
const overdueRes = await reportService.getOverdue();
setOverdueData(overdueRes);
break;
}
} catch (error) {
console.error('Error loading report data:', error);
} finally {
setLoading(false);
}
}, [activeTab, year, months]);
useEffect(() => {
loadData();
}, [loadData]);
const tabs = [
{ id: 'summary', label: t('reports.summary'), icon: 'bi-clipboard-data' },
{ id: 'category', label: t('reports.byCategory'), icon: 'bi-pie-chart' },
{ id: 'costCenter', label: t('reports.byCostCenter'), icon: 'bi-diagram-3' },
{ id: 'evolution', label: t('reports.monthlyEvolution'), icon: 'bi-graph-up' },
{ id: 'comparison', label: t('reports.comparison'), icon: 'bi-arrow-left-right' },
{ id: 'topExpenses', label: t('reports.topExpenses'), icon: 'bi-sort-down' },
{ id: 'projection', label: t('reports.projection'), icon: 'bi-lightning' },
{ id: 'recurring', label: t('reports.recurring'), icon: 'bi-arrow-repeat' },
{ id: 'liabilities', label: t('reports.liabilities'), icon: 'bi-credit-card' },
{ id: 'future', label: t('reports.futureTransactions'), icon: 'bi-calendar-plus' },
{ id: 'overdue', label: t('reports.overdue'), icon: 'bi-exclamation-triangle' },
];
// Chart options
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: { color: '#94a3b8' }
}
},
scales: {
x: {
ticks: { color: '#94a3b8' },
grid: { color: 'rgba(148, 163, 184, 0.1)' }
},
y: {
ticks: { color: '#94a3b8' },
grid: { color: 'rgba(148, 163, 184, 0.1)' }
}
}
};
const doughnutOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#94a3b8', padding: 15, font: { size: 11 } }
}
}
};
// Render Summary Tab
const renderSummary = () => {
if (!summary) return null;
const savingsRate = summary.current.income > 0
? ((summary.current.balance / summary.current.income) * 100).toFixed(1)
: 0;
const monthlyAvgIncome = (summary.current.income / 12).toFixed(2);
const monthlyAvgExpense = (summary.current.expense / 12).toFixed(2);
const monthlyAvgBalance = (summary.current.balance / 12).toFixed(2);
return (
{/* Year Selector */}
{t('reports.annualSummary')}
{[2024, 2025].map(y => (
))}
{/* Main KPI Cards */}
{t('reports.income')}
{currency(summary.current.income, summary.currency)}
{summary.variation.income !== 0 && !isNaN(summary.variation.income) && isFinite(summary.variation.income) && (
= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
= 0 ? 'up' : 'down'} me-1`}>
{Math.abs(summary.variation.income).toFixed(1)}%
{t('reports.vsLastYear')}
)}
{t('reports.monthlyAverage')}:
{currency(monthlyAvgIncome, summary.currency)}
{t('reports.expenses')}
{currency(summary.current.expense, summary.currency)}
{summary.variation.expense !== 0 && !isNaN(summary.variation.expense) && isFinite(summary.variation.expense) && (
= 0 ? 'up' : 'down'} me-1`}>
{Math.abs(summary.variation.expense).toFixed(1)}%
{t('reports.vsLastYear')}
)}
{t('reports.monthlyAverage')}:
{currency(monthlyAvgExpense, summary.currency)}
{t('reports.balance')}
{currency(summary.current.balance, summary.currency)}
{summary.variation.balance !== 0 && !isNaN(summary.variation.balance) && isFinite(summary.variation.balance) && (
= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
= 0 ? 'up' : 'down'} me-1`}>
{Math.abs(summary.variation.balance).toFixed(1)}%
{t('reports.vsLastYear')}
)}
{t('reports.monthlyAverage')}:
{currency(monthlyAvgBalance, summary.currency)}
{t('reports.savingsRate')}
{savingsRate}%
{savingsRate >= 20 ? '🎯 ' + t('reports.excellentSavings') :
savingsRate >= 10 ? '👍 ' + t('reports.goodSavings') :
'💡 ' + t('reports.canImprove')}
{/* Comparison Chart */}
{t('reports.yearComparison')} - {year-1} vs {year}
{t('reports.total')}: {currency(summary.current.balance, summary.currency)}
);
};
// Render Category Tab
const renderCategory = () => {
if (!categoryData) return null;
const colors = categoryData.data.map((_, i) =>
`hsl(${(i * 360) / categoryData.data.length}, 70%, 50%)`
);
return (
{t('reports.expenseDistribution')}
c.category_name),
datasets: [{
data: categoryData.data.map(c => c.total),
backgroundColor: colors,
borderWidth: 0,
}],
}}
options={doughnutOptions}
/>
{t('reports.categoryDetail')}
{currency(categoryData.total, categoryData.currency)}
| {t('reports.category')} |
{t('common.total')} |
% |
{categoryData.data.map((cat, i) => (
|
{cat.category_name}
|
{currency(cat.total, categoryData.currency)} |
{cat.percentage}%
|
))}
);
};
// Render Evolution Tab
const renderEvolution = () => {
if (!evolutionData) return null;
return (
{[6, 12, 24].map(m => (
))}
{/* Averages Cards */}
{t('reports.avgIncome')}
{currency(evolutionData.averages.income, evolutionData.currency)}
{t('reports.avgExpense')}
{currency(evolutionData.averages.expense, evolutionData.currency)}
{t('reports.balance')}
= 0 ? 'text-success' : 'text-danger'}`}>
{currency(evolutionData.averages.balance, evolutionData.currency)}
{t('reports.savingsRate')}
{evolutionData.averages.savings_rate}%
{/* Evolution Chart */}
{t('reports.monthlyEvolution')}
d.month_label),
datasets: [
{
label: t('reports.income'),
data: evolutionData.data.map(d => d.income),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.3,
},
{
label: t('reports.expenses'),
data: evolutionData.data.map(d => d.expense),
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
fill: true,
tension: 0.3,
},
{
label: t('reports.balance'),
data: evolutionData.data.map(d => d.balance),
borderColor: '#3b82f6',
backgroundColor: 'transparent',
borderDash: [5, 5],
tension: 0.3,
},
],
}}
options={chartOptions}
/>
{/* Savings Rate Chart */}
{t('reports.savingsRate')} por mes
d.month_label),
datasets: [{
label: t('reports.savingsRate'),
data: evolutionData.data.map(d => d.savings_rate),
backgroundColor: evolutionData.data.map(d =>
d.savings_rate >= 20 ? 'rgba(16, 185, 129, 0.7)' :
d.savings_rate >= 10 ? 'rgba(245, 158, 11, 0.7)' :
d.savings_rate >= 0 ? 'rgba(239, 68, 68, 0.5)' :
'rgba(239, 68, 68, 0.8)'
),
borderRadius: 4,
}],
}}
options={{
...chartOptions,
plugins: {
...chartOptions.plugins,
legend: { display: false }
}
}}
/>
);
};
// Render Comparison Tab
const renderComparison = () => {
if (!comparison) return null;
return (
{/* Header com seletor de períodos */}
{t('reports.periodComparison')}
{/* Cards KPI Comparativos */}
{t('reports.income')}
{currency(comparison.period1.income, comparison.currency)}
{comparison.variation.income !== 0 && !isNaN(comparison.variation.income) && isFinite(comparison.variation.income) && (
<>
= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
= 0 ? 'up' : 'down'} me-1`}>
{comparison.variation.income > 0 ? '+' : ''}{Math.abs(comparison.variation.income).toFixed(1)}%
{t('reports.vsPreviousPeriod')}
>
)}
{comparison.period2.label}:
{currency(comparison.period2.income, comparison.currency)}
{t('reports.expenses')}
{currency(comparison.period1.expense, comparison.currency)}
{comparison.variation.expense !== 0 && !isNaN(comparison.variation.expense) && isFinite(comparison.variation.expense) && (
<>
= 0 ? 'up' : 'down'} me-1`}>
{comparison.variation.expense > 0 ? '+' : ''}{Math.abs(comparison.variation.expense).toFixed(1)}%
{t('reports.vsPreviousPeriod')}
>
)}
{comparison.period2.label}:
{currency(comparison.period2.expense, comparison.currency)}
{t('reports.balance')}
{currency(comparison.period1.balance, comparison.currency)}
{comparison.variation.balance !== 0 && !isNaN(comparison.variation.balance) && isFinite(comparison.variation.balance) && (
<>
= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
= 0 ? 'up' : 'down'} me-1`}>
{comparison.variation.balance > 0 ? '+' : ''}{Math.abs(comparison.variation.balance).toFixed(1)}%
{t('reports.vsPreviousPeriod')}
>
)}
{comparison.period2.label}:
{currency(comparison.period2.balance, comparison.currency)}
{/* Tabela Comparativa Detalhada */}
{t('reports.detailedComparison')}
| {t('reports.metric')} |
{comparison.period2.label} |
{comparison.period1.label} |
{t('reports.variation')} |
| {t('reports.income')} |
{currency(comparison.period2.income, comparison.currency)} |
{currency(comparison.period1.income, comparison.currency)} |
{comparison.variation.income !== 0 && !isNaN(comparison.variation.income) && isFinite(comparison.variation.income) && (
= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.income > 0 ? '+' : ''}{comparison.variation.income.toFixed(1)}%
)}
|
| {t('reports.expenses')} |
{currency(comparison.period2.expense, comparison.currency)} |
{currency(comparison.period1.expense, comparison.currency)} |
{comparison.variation.expense !== 0 && !isNaN(comparison.variation.expense) && isFinite(comparison.variation.expense) && (
{comparison.variation.expense > 0 ? '+' : ''}{comparison.variation.expense.toFixed(1)}%
)}
|
| {t('reports.balance')} |
= 0 ? 'text-success' : 'text-danger'}`}>
{currency(comparison.period2.balance, comparison.currency)}
|
= 0 ? 'text-success' : 'text-danger'}`}>
{currency(comparison.period1.balance, comparison.currency)}
|
{comparison.variation.balance !== 0 && !isNaN(comparison.variation.balance) && isFinite(comparison.variation.balance) && (
= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.balance > 0 ? '+' : ''}{comparison.variation.balance.toFixed(1)}%
)}
|
{/* Gráfico de Barras Agrupadas */}
{t('reports.visualComparison')}
);
};
// Render Top Expenses Tab
const renderTopExpenses = () => {
if (!topExpenses) return null;
return (
{t('reports.top20Expenses')}
{currency(topExpenses.total, topExpenses.currency)}
| # |
{t('reports.description')} |
{t('reports.category')} |
{t('reports.date')} |
{t('reports.amount')} |
{topExpenses.data.map((item, i) => (
| {i + 1} |
{item.description} |
{item.category || '-'} |
{item.date} |
{currency(item.amount, item.currency || topExpenses.currency)} |
))}
);
};
// Render Projection Tab
const renderProjection = () => {
return (
{/* Balance Projection Chart - Full Width First */}
{projection && (
<>
{t('reports.currentMonth')}
{t('reports.income')}
{currency(projection.current_month.income, projection.currency)}
{t('reports.expenses')}
{currency(projection.current_month.expense, projection.currency)}
{t('reports.daysRemaining')}
{projection.current_month.days_remaining} {t('common.days')}
{t('reports.projectionTitle')}
{t('reports.projectedIncome')}
{currency(projection.projection.income, projection.currency)}
{t('reports.projectedExpense')}
{currency(projection.projection.expense, projection.currency)}
{t('reports.balance')}
= 0 ? '' : 'text-warning'}`}>
{currency(projection.projection.balance, projection.currency)}
{/* vs Average */}
{t('reports.vsAverage')} ({t('reports.last3Months')})
>
)}
);
};
// Render Day of Week Tab
const renderDayOfWeek = () => {
if (!dayOfWeekData || !dayOfWeekData.data) return null;
const data = dayOfWeekData.data;
return (
{t('reports.expensesByDayOfWeek')}
t(`reports.dayOfWeek.${d.day_key}`)),
datasets: [{
label: t('reports.totalSpent'),
data: data.map(d => d.total),
backgroundColor: data.map(d =>
d.day_num === 1 || d.day_num === 7
? 'rgba(245, 158, 11, 0.7)'
: 'rgba(59, 130, 246, 0.7)'
),
borderRadius: 4,
}],
}}
options={{
...chartOptions,
plugins: { ...chartOptions.plugins, legend: { display: false } }
}}
/>
| {t('reports.dayOfWeek.day')} |
{t('transactions.title')} |
{t('common.total')} |
{t('reports.avgExpense')} |
{dayOfWeekData.data.map(d => (
|
{t(`reports.dayOfWeek.${d.day_key}`)}
|
{d.count} |
{currency(d.total, dayOfWeekData.currency)} |
{currency(d.average, dayOfWeekData.currency)} |
))}
);
};
// Render Cost Center Tab
const renderCostCenter = () => {
if (!costCenterData) return null;
const data = costCenterData.data || [];
return (
{t('reports.totalIncome')}
{currency(costCenterData.total_income || 0, costCenterData.currency)}
{t('reports.totalExpense')}
{currency(costCenterData.total_expense || 0, costCenterData.currency)}
{t('reports.balance')}
= 0 ? 'text-success' : 'text-danger'}`}>
{currency((costCenterData.total_income || 0) - (costCenterData.total_expense || 0), costCenterData.currency)}
{t('reports.byCostCenter')}
| {t('costCenters.name')} |
{t('reports.income')} |
{t('reports.expenses')} |
{t('reports.balance')} |
{data.map(cc => (
|
●
{cc.name}
|
{currency(cc.income, costCenterData.currency)} |
{currency(cc.expense, costCenterData.currency)} |
= 0 ? 'text-success' : 'text-danger'}`}>
{currency(cc.balance, costCenterData.currency)}
|
))}
);
};
// Render Recurring Tab
const renderRecurring = () => {
if (!recurringData) return null;
const templates = recurringData.templates || [];
const summary = recurringData.summary || {};
return (
{t('reports.totalRecurring')}
{summary.total_recurring || 0}
{t('reports.monthlyIncome')}
{currency(summary.monthly_income || 0, recurringData.currency)}
{t('reports.monthlyExpense')}
{currency(summary.monthly_expense || 0, recurringData.currency)}
{t('reports.netRecurring')}
= 0 ? 'text-success' : 'text-danger'}`}>
{currency(summary.net_recurring || 0, recurringData.currency)}
{t('reports.recurringList')}
| {t('common.description')} |
{t('reports.category')} |
{t('recurring.frequency')} |
{t('reports.nextDate')} |
{t('reports.amount')} |
{templates.map(t => (
| {t.description} |
{t.category && (
{t.category}
)}
|
{t.frequency} |
{t.next_date} |
{t.type === 'credit' ? '+' : '-'}{currency(t.amount, t.currency)}
|
))}
);
};
// Render Liabilities Tab
const renderLiabilities = () => {
if (!liabilitiesData) return null;
const data = liabilitiesData.data || [];
const summary = liabilitiesData.summary || {};
return (
{t('reports.totalLiabilities')}
{summary.total_liabilities || 0}
{t('reports.totalDebt')}
{currency(summary.total_debt || 0, liabilitiesData.currency)}
{t('reports.totalPaid')}
{currency(summary.total_paid || 0, liabilitiesData.currency)}
{t('reports.totalPending')}
{currency(summary.total_pending || 0, liabilitiesData.currency)}
{data.map(liability => (
{liability.name}
{liability.type}
{liability.overdue_installments > 0 && (
{liability.overdue_installments} {t('reports.overdueInstallments')}
)}
{liability.paid_installments}/{liability.total_installments} {t('reports.installments')}
{liability.progress?.toFixed(1)}%
{t('common.total')}
{currency(liability.total_amount, liability.currency)}
{t('reports.paid')}
{currency(liability.paid_amount, liability.currency)}
{t('reports.pending')}
{currency(liability.pending_amount, liability.currency)}
{liability.next_installment && (
{t('reports.nextInstallment')}:
{formatDate(liability.next_installment.due_date)}
{currency(liability.next_installment.amount, liability.currency)}
)}
))}
);
};
// Render Future Transactions Tab
const renderFuture = () => {
if (!futureData) return null;
const data = futureData.data || [];
const summary = futureData.summary || {};
return (
{t('reports.totalTransactions')}
{summary.total_transactions || 0}
{t('reports.futureIncome')}
{currency(summary.total_income || 0, futureData.currency)}
{t('reports.futureExpense')}
{currency(summary.total_expense || 0, futureData.currency)}
{t('reports.netImpact')}
= 0 ? 'text-success' : 'text-danger'}`}>
{currency(summary.net_impact || 0, futureData.currency)}
{t('reports.next30Days')}
| {t('reports.date')} |
{t('common.description')} |
{t('reports.category')} |
{t('reports.account')} |
{t('reports.amount')} |
{data.map(tx => (
|
{tx.days_until}d
{tx.date}
|
{tx.description} |
{tx.category && (
{tx.category}
)}
|
{tx.account} |
{tx.type === 'credit' ? '+' : '-'}{currency(tx.amount, tx.currency)}
|
))}
);
};
// Render Overdue Tab
const renderOverdue = () => {
if (!overdueData) return null;
const data = overdueData.data || [];
const summary = overdueData.summary || {};
return (
{t('reports.totalOverdue')}
{summary.total_overdue || 0}
{t('reports.overdueAmount')}
{currency(summary.total_amount || 0, overdueData.currency)}
{data.length === 0 ? (
{t('reports.noOverdue')}
{t('reports.noOverdueDescription')}
) : (
{t('reports.overdueList')}
| {t('common.description')} |
{t('reports.dueDate')} |
{t('reports.daysOverdue')} |
{t('reports.amount')} |
{data.map(item => (
| {item.description} |
{item.due_date} |
{item.days_overdue} {t('common.days')}
|
{currency(item.amount, item.currency)}
|
))}
)}
);
};
const renderContent = () => {
if (loading) {
return (
);
}
switch (activeTab) {
case 'summary': return renderSummary();
case 'category': return renderCategory();
case 'costCenter': return renderCostCenter();
case 'evolution': return renderEvolution();
case 'comparison': return renderComparison();
case 'topExpenses': return renderTopExpenses();
case 'projection': return renderProjection();
case 'recurring': return renderRecurring();
case 'liabilities': return renderLiabilities();
case 'future': return renderFuture();
case 'overdue': return renderOverdue();
default: return null;
}
};
return (
{/* Header */}
{t('reports.title')}
{t('reports.subtitle')}
{/* Tabs */}
{tabs.map(tab => (
))}
{/* Content */}
{renderContent()}
);
};
export default Reports;