v1.36.0 - Redesign seção Maiores Despesas com KPI cards + gráfico barras + tabela profissional

This commit is contained in:
marcoitaloesp-ai 2025-12-15 16:18:52 +00:00 committed by GitHub
parent d03565d4ab
commit 7fbd572371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 261 additions and 32 deletions

View File

@ -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

View File

@ -1 +1 @@
1.35.0
1.36.0

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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>