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/).
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
### Improved
|
||||
|
||||
@ -1860,6 +1860,13 @@
|
||||
"description": "Description",
|
||||
"date": "Date",
|
||||
"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",
|
||||
"totalSpent": "Total spent",
|
||||
"totalIncome": "Total Income",
|
||||
|
||||
@ -1843,6 +1843,13 @@
|
||||
"description": "Descripción",
|
||||
"date": "Fecha",
|
||||
"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",
|
||||
"totalSpent": "Total gastado",
|
||||
"totalIncome": "Total Ingresos",
|
||||
|
||||
@ -1862,6 +1862,13 @@
|
||||
"description": "Descrição",
|
||||
"date": "Data",
|
||||
"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",
|
||||
"totalSpent": "Total gasto",
|
||||
"totalIncome": "Total Receitas",
|
||||
|
||||
@ -874,39 +874,231 @@ const Reports = () => {
|
||||
const renderTopExpenses = () => {
|
||||
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 (
|
||||
<div className="card border-0" style={{ background: '#0f172a' }}>
|
||||
<div className="card-header border-0 bg-transparent d-flex justify-content-between">
|
||||
<h6 className="text-white mb-0">
|
||||
<i className="bi bi-sort-down me-2"></i>
|
||||
{t('reports.top20Expenses')}
|
||||
</h6>
|
||||
<span className="text-danger fw-bold">{currency(topExpenses.total, topExpenses.currency)}</span>
|
||||
<div className="row g-4">
|
||||
{/* Header */}
|
||||
<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>
|
||||
{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>
|
||||
<div className="card-body p-0">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{t('reports.description')}</th>
|
||||
<th>{t('reports.category')}</th>
|
||||
<th>{t('reports.date')}</th>
|
||||
<th className="text-end">{t('reports.amount')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topExpenses.data.map((item, i) => (
|
||||
<tr key={item.id}>
|
||||
<td><span className="badge bg-secondary">{i + 1}</span></td>
|
||||
<td className="text-truncate" style={{ maxWidth: '200px' }}>{item.description}</td>
|
||||
<td><span className="badge bg-primary">{item.category || '-'}</span></td>
|
||||
<td className="text-slate-400">{item.date}</td>
|
||||
<td className="text-end text-danger fw-bold">{currency(item.amount, item.currency || topExpenses.currency)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
<div className="card-body p-0">
|
||||
<div className="table-responsive" style={{ maxHeight: '500px' }}>
|
||||
<table className="table table-dark table-hover mb-0">
|
||||
<thead className="sticky-top" style={{ background: '#1e293b' }}>
|
||||
<tr>
|
||||
<th className="text-center" style={{ width: '60px' }}>#</th>
|
||||
<th>{t('reports.description')}</th>
|
||||
<th>{t('reports.category')}</th>
|
||||
<th className="text-center">{t('reports.date')}</th>
|
||||
<th className="text-end">{t('reports.amount')}</th>
|
||||
<th className="text-center" style={{ width: '100px' }}>% {t('reports.total')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topExpenses.data.map((item, i) => {
|
||||
const percentage = (item.amount / topExpenses.total) * 100;
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td className="text-center">
|
||||
<span className={`badge ${i < 3 ? 'bg-danger' : i < 10 ? 'bg-warning text-dark' : 'bg-secondary'}`}>
|
||||
{i + 1}
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user