v1.36.0 - Redesign seção Maiores Despesas com KPI cards + gráfico barras + tabela profissional
This commit is contained in:
parent
d03565d4ab
commit
7fbd572371
16
CHANGELOG.md
16
CHANGELOG.md
@ -5,6 +5,22 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
|
|||||||
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
|
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.36.0] - 2025-12-15
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- **UI/UX da Seção Maiores Despesas** - Interface completamente redesenhada
|
||||||
|
- 4 Cards KPI com gradientes: Maior Despesa, Média, Menor Despesa, Total de Transações
|
||||||
|
- Gráfico de Barras Horizontais mostrando Top 10 despesas com degradê de cores
|
||||||
|
- Tabela detalhada completa com:
|
||||||
|
* Badges coloridos para posições (top 3 em vermelho, top 10 em amarelo)
|
||||||
|
* Barra de progresso visual mostrando % do total
|
||||||
|
* Categorias em badges
|
||||||
|
* Formatação profissional com sticky header
|
||||||
|
* Scroll independente para listas longas
|
||||||
|
- Layout responsivo em grid com altura controlada
|
||||||
|
- Tooltips do gráfico com informações completas (valor, categoria, data)
|
||||||
|
- Traduções: topExpensesAnalysis, highestExpense, averageExpense, lowestExpense, totalTransactions, top10Expenses, detailedList
|
||||||
|
|
||||||
## [1.35.0] - 2025-12-15
|
## [1.35.0] - 2025-12-15
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
|
|||||||
@ -1860,6 +1860,13 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"top20Expenses": "Top 20 Monthly Expenses",
|
"top20Expenses": "Top 20 Monthly Expenses",
|
||||||
|
"topExpensesAnalysis": "Top Expenses Analysis",
|
||||||
|
"highestExpense": "Highest Expense",
|
||||||
|
"averageExpense": "Average Expense",
|
||||||
|
"lowestExpense": "Lowest Expense",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"top10Expenses": "Top 10 Expenses",
|
||||||
|
"detailedList": "Detailed List",
|
||||||
"expensesByDayOfWeek": "Expenses by Day of Week",
|
"expensesByDayOfWeek": "Expenses by Day of Week",
|
||||||
"totalSpent": "Total spent",
|
"totalSpent": "Total spent",
|
||||||
"totalIncome": "Total Income",
|
"totalIncome": "Total Income",
|
||||||
|
|||||||
@ -1843,6 +1843,13 @@
|
|||||||
"description": "Descripción",
|
"description": "Descripción",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
"top20Expenses": "Top 20 Gastos del Mes",
|
"top20Expenses": "Top 20 Gastos del Mes",
|
||||||
|
"topExpensesAnalysis": "Análisis de Mayores Gastos",
|
||||||
|
"highestExpense": "Mayor Gasto",
|
||||||
|
"averageExpense": "Promedio de Gastos",
|
||||||
|
"lowestExpense": "Menor Gasto",
|
||||||
|
"totalTransactions": "Total de Transacciones",
|
||||||
|
"top10Expenses": "Top 10 Mayores Gastos",
|
||||||
|
"detailedList": "Lista Detallada",
|
||||||
"expensesByDayOfWeek": "Gastos por Día de la Semana",
|
"expensesByDayOfWeek": "Gastos por Día de la Semana",
|
||||||
"totalSpent": "Total gastado",
|
"totalSpent": "Total gastado",
|
||||||
"totalIncome": "Total Ingresos",
|
"totalIncome": "Total Ingresos",
|
||||||
|
|||||||
@ -1862,6 +1862,13 @@
|
|||||||
"description": "Descrição",
|
"description": "Descrição",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
"top20Expenses": "Top 20 Despesas do Mês",
|
"top20Expenses": "Top 20 Despesas do Mês",
|
||||||
|
"topExpensesAnalysis": "Análise das Maiores Despesas",
|
||||||
|
"highestExpense": "Maior Despesa",
|
||||||
|
"averageExpense": "Média das Despesas",
|
||||||
|
"lowestExpense": "Menor Despesa",
|
||||||
|
"totalTransactions": "Total de Transações",
|
||||||
|
"top10Expenses": "Top 10 Maiores Despesas",
|
||||||
|
"detailedList": "Lista Detalhada",
|
||||||
"expensesByDayOfWeek": "Despesas por Dia da Semana",
|
"expensesByDayOfWeek": "Despesas por Dia da Semana",
|
||||||
"totalSpent": "Total gasto",
|
"totalSpent": "Total gasto",
|
||||||
"totalIncome": "Total Receitas",
|
"totalIncome": "Total Receitas",
|
||||||
|
|||||||
@ -874,42 +874,234 @@ const Reports = () => {
|
|||||||
const renderTopExpenses = () => {
|
const renderTopExpenses = () => {
|
||||||
if (!topExpenses) return null;
|
if (!topExpenses) return null;
|
||||||
|
|
||||||
|
const top10 = topExpenses.data.slice(0, 10);
|
||||||
|
const averageExpense = topExpenses.total / topExpenses.data.length;
|
||||||
|
const maxExpense = topExpenses.data.length > 0 ? topExpenses.data[0].amount : 0;
|
||||||
|
const minExpense = topExpenses.data.length > 0 ? topExpenses.data[topExpenses.data.length - 1].amount : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card border-0" style={{ background: '#0f172a' }}>
|
<div className="row g-4">
|
||||||
<div className="card-header border-0 bg-transparent d-flex justify-content-between">
|
{/* Header */}
|
||||||
<h6 className="text-white mb-0">
|
<div className="col-12">
|
||||||
|
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h5 className="text-white mb-0">
|
||||||
<i className="bi bi-sort-down me-2"></i>
|
<i className="bi bi-sort-down me-2"></i>
|
||||||
{t('reports.top20Expenses')}
|
{t('reports.topExpensesAnalysis')}
|
||||||
|
</h5>
|
||||||
|
<span className="badge bg-danger fs-6 px-3 py-2">
|
||||||
|
{t('reports.total')}: {currency(topExpenses.total, topExpenses.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards KPI */}
|
||||||
|
<div className="col-lg-3 col-md-6">
|
||||||
|
<div className="card border-0" style={{ background: 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)' }}>
|
||||||
|
<div className="card-body text-white">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.highestExpense')}</div>
|
||||||
|
<h4 className="mb-0 fw-bold">{currency(maxExpense, topExpenses.currency)}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-lg-3 col-md-6">
|
||||||
|
<div className="card border-0" style={{ background: 'linear-gradient(135deg, #F59E0B 0%, #D97706 100%)' }}>
|
||||||
|
<div className="card-body text-white">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.averageExpense')}</div>
|
||||||
|
<h4 className="mb-0 fw-bold">{currency(averageExpense, topExpenses.currency)}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-graph-down"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-lg-3 col-md-6">
|
||||||
|
<div className="card border-0" style={{ background: 'linear-gradient(135deg, #10B981 0%, #059669 100%)' }}>
|
||||||
|
<div className="card-body text-white">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.lowestExpense')}</div>
|
||||||
|
<h4 className="mb-0 fw-bold">{currency(minExpense, topExpenses.currency)}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-lg-3 col-md-6">
|
||||||
|
<div className="card border-0" style={{ background: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)' }}>
|
||||||
|
<div className="card-body text-white">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.totalTransactions')}</div>
|
||||||
|
<h4 className="mb-0 fw-bold">{topExpenses.data.length}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-list-ul"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gráfico de Barras Horizontais - Top 10 */}
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="card border-0" style={{ background: '#0f172a' }}>
|
||||||
|
<div className="card-header border-0 bg-transparent">
|
||||||
|
<h6 className="text-white mb-0">
|
||||||
|
<i className="bi bi-bar-chart-line me-2"></i>
|
||||||
|
{t('reports.top10Expenses')}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div className="card-body" style={{ height: '400px' }}>
|
||||||
|
<Bar
|
||||||
|
data={{
|
||||||
|
labels: top10.map((item, i) => `${i + 1}. ${item.description.substring(0, 30)}${item.description.length > 30 ? '...' : ''}`),
|
||||||
|
datasets: [{
|
||||||
|
label: t('reports.amount'),
|
||||||
|
data: top10.map(item => item.amount),
|
||||||
|
backgroundColor: top10.map((_, i) => {
|
||||||
|
const opacity = 1 - (i * 0.08);
|
||||||
|
return `rgba(239, 68, 68, ${opacity})`;
|
||||||
|
}),
|
||||||
|
borderColor: '#EF4444',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 6,
|
||||||
|
}],
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#94a3b8',
|
||||||
|
borderColor: '#334155',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 12,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
const item = top10[context.dataIndex];
|
||||||
|
return [
|
||||||
|
`${t('reports.amount')}: ${currency(context.parsed.x, topExpenses.currency)}`,
|
||||||
|
`${t('reports.category')}: ${item.category || '-'}`,
|
||||||
|
`${t('reports.date')}: ${item.date}`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: '#94a3b8',
|
||||||
|
callback: function(value) {
|
||||||
|
return currency(value, topExpenses.currency);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { color: 'rgba(148, 163, 184, 0.1)' }
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: '#94a3b8',
|
||||||
|
font: { size: 11 }
|
||||||
|
},
|
||||||
|
grid: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabela Detalhada */}
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="card border-0" style={{ background: '#0f172a' }}>
|
||||||
|
<div className="card-header border-0 bg-transparent">
|
||||||
|
<h6 className="text-white mb-0">
|
||||||
|
<i className="bi bi-table me-2"></i>
|
||||||
|
{t('reports.detailedList')} ({topExpenses.data.length} {t('reports.transactions')})
|
||||||
</h6>
|
</h6>
|
||||||
<span className="text-danger fw-bold">{currency(topExpenses.total, topExpenses.currency)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body p-0">
|
<div className="card-body p-0">
|
||||||
<div className="table-responsive">
|
<div className="table-responsive" style={{ maxHeight: '500px' }}>
|
||||||
<table className="table table-dark table-hover mb-0">
|
<table className="table table-dark table-hover mb-0">
|
||||||
<thead>
|
<thead className="sticky-top" style={{ background: '#1e293b' }}>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th className="text-center" style={{ width: '60px' }}>#</th>
|
||||||
<th>{t('reports.description')}</th>
|
<th>{t('reports.description')}</th>
|
||||||
<th>{t('reports.category')}</th>
|
<th>{t('reports.category')}</th>
|
||||||
<th>{t('reports.date')}</th>
|
<th className="text-center">{t('reports.date')}</th>
|
||||||
<th className="text-end">{t('reports.amount')}</th>
|
<th className="text-end">{t('reports.amount')}</th>
|
||||||
|
<th className="text-center" style={{ width: '100px' }}>% {t('reports.total')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{topExpenses.data.map((item, i) => (
|
{topExpenses.data.map((item, i) => {
|
||||||
|
const percentage = (item.amount / topExpenses.total) * 100;
|
||||||
|
return (
|
||||||
<tr key={item.id}>
|
<tr key={item.id}>
|
||||||
<td><span className="badge bg-secondary">{i + 1}</span></td>
|
<td className="text-center">
|
||||||
<td className="text-truncate" style={{ maxWidth: '200px' }}>{item.description}</td>
|
<span className={`badge ${i < 3 ? 'bg-danger' : i < 10 ? 'bg-warning text-dark' : 'bg-secondary'}`}>
|
||||||
<td><span className="badge bg-primary">{item.category || '-'}</span></td>
|
{i + 1}
|
||||||
<td className="text-slate-400">{item.date}</td>
|
</span>
|
||||||
<td className="text-end text-danger fw-bold">{currency(item.amount, item.currency || topExpenses.currency)}</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="text-white text-truncate" style={{ maxWidth: '300px' }}>
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="badge bg-primary">{item.category || '-'}</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-center text-slate-400 small">{item.date}</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<span className="text-danger fw-bold">
|
||||||
|
{currency(item.amount, item.currency || topExpenses.currency)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<div className="d-flex align-items-center justify-content-center gap-2">
|
||||||
|
<div className="progress flex-grow-1" style={{ height: '6px', width: '50px' }}>
|
||||||
|
<div
|
||||||
|
className="progress-bar bg-danger"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<small className="text-slate-400" style={{ minWidth: '45px' }}>
|
||||||
|
{percentage.toFixed(1)}%
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user