v1.34.1: UI/UX melhorada do resumo de relatórios
Improved: - Cards KPI redesenhados com ícones e melhor hierarquia - Badges de variação com contraste aprimorado - Métricas de média mensal adicionadas - Card de Taxa de Poupança com barra de progresso - Feedback inteligente (Excelente/Boa/Pode melhorar) - Gráfico comparativo com tooltips e bordas arredondadas - Tradução completa (pt-BR, en, es)
This commit is contained in:
parent
f9571656d5
commit
82e1d7a884
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.34.1] - 2025-12-15
|
||||
|
||||
### Improved
|
||||
- **UI/UX do Resumo de Relatórios** - Interface redesenhada com visual mais profissional
|
||||
- Cards KPI com ícones grandes e hierarquia visual clara
|
||||
- Badges de variação com fundo branco para melhor contraste
|
||||
- Adicionadas métricas de média mensal em todos os cards
|
||||
- Novo card dedicado para Taxa de Poupança com barra de progresso
|
||||
- Feedback visual inteligente baseado na taxa de poupança:
|
||||
* 🎯 ≥20%: "Excelente poupança!"
|
||||
* 👍 ≥10%: "Boa poupança"
|
||||
* 💡 <10%: "Pode melhorar"
|
||||
- Gráfico comparativo melhorado com bordas arredondadas e tooltips aprimorados
|
||||
- Header do resumo com ícone e seletor de ano redesenhado
|
||||
- Tradução completa: pt-BR, en, es
|
||||
|
||||
## [1.34.0] - 2025-12-14
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -1834,7 +1834,14 @@
|
||||
"topExpenses": "Top Expenses",
|
||||
"vsAverage": "vs Average",
|
||||
"vsLastPeriod": "vs Last Period",
|
||||
"vsLastYear": "vs last year",
|
||||
"yearComparison": "Year Comparison",
|
||||
"annualSummary": "Annual Summary",
|
||||
"monthlyAverage": "Monthly Average",
|
||||
"excellentSavings": "Excellent savings!",
|
||||
"goodSavings": "Good savings",
|
||||
"canImprove": "Can improve",
|
||||
"total": "Total",
|
||||
"expenseDistribution": "Expense Distribution",
|
||||
"categoryDetail": "Category Detail",
|
||||
"category": "Category",
|
||||
|
||||
@ -1801,6 +1801,14 @@
|
||||
"avgIncome": "Ingreso promedio",
|
||||
"avgExpense": "Gasto promedio",
|
||||
"vsLastPeriod": "vs período anterior",
|
||||
"vsLastYear": "vs año anterior",
|
||||
"yearComparison": "Comparación Anual",
|
||||
"annualSummary": "Resumen Anual",
|
||||
"monthlyAverage": "Promedio Mensual",
|
||||
"excellentSavings": "¡Excelente ahorro!",
|
||||
"goodSavings": "Buen ahorro",
|
||||
"canImprove": "Puede mejorar",
|
||||
"total": "Total",
|
||||
"dayOfWeek": {
|
||||
"sunday": "Domingo",
|
||||
"monday": "Lunes",
|
||||
|
||||
@ -1836,7 +1836,14 @@
|
||||
"topExpenses": "Maiores Despesas",
|
||||
"vsAverage": "vs Média",
|
||||
"vsLastPeriod": "vs Período Anterior",
|
||||
"vsLastYear": "vs ano anterior",
|
||||
"yearComparison": "Comparativo Anual",
|
||||
"annualSummary": "Resumo Anual",
|
||||
"monthlyAverage": "Média Mensal",
|
||||
"excellentSavings": "Excelente poupança!",
|
||||
"goodSavings": "Boa poupança",
|
||||
"canImprove": "Pode melhorar",
|
||||
"total": "Total",
|
||||
"expenseDistribution": "Distribuição de Despesas",
|
||||
"categoryDetail": "Detalhes por Categoria",
|
||||
"category": "Categoria",
|
||||
|
||||
@ -169,64 +169,152 @@ const Reports = () => {
|
||||
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 (
|
||||
<div className="row g-4">
|
||||
{/* Year Selector */}
|
||||
<div className="col-12">
|
||||
<div className="d-flex gap-2 mb-3">
|
||||
{[2024, 2025].map(y => (
|
||||
<button
|
||||
key={y}
|
||||
className={`btn btn-sm ${year === y ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||
onClick={() => setYear(y)}
|
||||
>
|
||||
{y}
|
||||
</button>
|
||||
))}
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<h5 className="text-white mb-0">
|
||||
<i className="bi bi-calendar3 me-2"></i>
|
||||
{t('reports.annualSummary')}
|
||||
</h5>
|
||||
<div className="btn-group" role="group">
|
||||
{[2024, 2025].map(y => (
|
||||
<button
|
||||
key={y}
|
||||
className={`btn btn-sm ${year === y ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||
onClick={() => setYear(y)}
|
||||
>
|
||||
{y}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="col-md-4">
|
||||
{/* Main KPI Cards */}
|
||||
<div className="col-lg-3 col-md-6">
|
||||
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #059669 0%, #047857 100%)' }}>
|
||||
<div className="card-body text-white">
|
||||
<h6 className="opacity-75">{t('reports.income')} {year}</h6>
|
||||
<h3 className="mb-2">{currency(summary.current.income, summary.currency)}</h3>
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.income')}</div>
|
||||
<h3 className="mb-0 fw-bold">{currency(summary.current.income, summary.currency)}</h3>
|
||||
</div>
|
||||
<div className="fs-1 opacity-50">
|
||||
<i className="bi bi-arrow-up-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
{summary.variation.income !== 0 && (
|
||||
<span className={`badge ${summary.variation.income >= 0 ? 'bg-success' : 'bg-danger'}`}>
|
||||
<i className={`bi bi-arrow-${summary.variation.income >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||
{Math.abs(summary.variation.income)}% {t('reports.vsLastPeriod')}
|
||||
</span>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className={`badge ${summary.variation.income >= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
|
||||
<i className={`bi bi-arrow-${summary.variation.income >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||
{Math.abs(summary.variation.income).toFixed(1)}%
|
||||
</span>
|
||||
<small className="opacity-75">{t('reports.vsLastYear')}</small>
|
||||
</div>
|
||||
)}
|
||||
<hr className="my-2 opacity-25" />
|
||||
<div className="small d-flex justify-content-between">
|
||||
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
||||
<span className="fw-semibold">{currency(monthlyAvgIncome, summary.currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<div className="col-lg-3 col-md-6">
|
||||
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)' }}>
|
||||
<div className="card-body text-white">
|
||||
<h6 className="opacity-75">{t('reports.expenses')} {year}</h6>
|
||||
<h3 className="mb-2">{currency(summary.current.expense, summary.currency)}</h3>
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.expenses')}</div>
|
||||
<h3 className="mb-0 fw-bold">{currency(summary.current.expense, summary.currency)}</h3>
|
||||
</div>
|
||||
<div className="fs-1 opacity-50">
|
||||
<i className="bi bi-arrow-down-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
{summary.variation.expense !== 0 && (
|
||||
<span className={`badge ${summary.variation.expense <= 0 ? 'bg-success' : 'bg-danger'}`}>
|
||||
<i className={`bi bi-arrow-${summary.variation.expense >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||
{Math.abs(summary.variation.expense)}% {t('reports.vsLastPeriod')}
|
||||
</span>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className={`badge ${summary.variation.expense <= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
|
||||
<i className={`bi bi-arrow-${summary.variation.expense >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||
{Math.abs(summary.variation.expense).toFixed(1)}%
|
||||
</span>
|
||||
<small className="opacity-75">{t('reports.vsLastYear')}</small>
|
||||
</div>
|
||||
)}
|
||||
<hr className="my-2 opacity-25" />
|
||||
<div className="small d-flex justify-content-between">
|
||||
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
||||
<span className="fw-semibold">{currency(monthlyAvgExpense, summary.currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<div className="col-lg-3 col-md-6">
|
||||
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' }}>
|
||||
<div className="card-body text-white">
|
||||
<h6 className="opacity-75">{t('reports.balance')} {year}</h6>
|
||||
<h3 className="mb-2">{currency(summary.current.balance, summary.currency)}</h3>
|
||||
<span className="small opacity-75">
|
||||
{t('reports.savingsRate')}: {summary.current.income > 0
|
||||
? ((summary.current.balance / summary.current.income) * 100).toFixed(1)
|
||||
: 0}%
|
||||
</span>
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.balance')}</div>
|
||||
<h3 className="mb-0 fw-bold">{currency(summary.current.balance, summary.currency)}</h3>
|
||||
</div>
|
||||
<div className="fs-1 opacity-50">
|
||||
<i className="bi bi-wallet2"></i>
|
||||
</div>
|
||||
</div>
|
||||
{summary.variation.balance !== 0 && (
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className={`badge ${summary.variation.balance >= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
|
||||
<i className={`bi bi-arrow-${summary.variation.balance >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||
{Math.abs(summary.variation.balance).toFixed(1)}%
|
||||
</span>
|
||||
<small className="opacity-75">{t('reports.vsLastYear')}</small>
|
||||
</div>
|
||||
)}
|
||||
<hr className="my-2 opacity-25" />
|
||||
<div className="small d-flex justify-content-between">
|
||||
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
||||
<span className="fw-semibold">{currency(monthlyAvgBalance, summary.currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-3 col-md-6">
|
||||
<div className="card border-0 h-100" 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 mb-3">
|
||||
<div>
|
||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.savingsRate')}</div>
|
||||
<h3 className="mb-0 fw-bold">{savingsRate}%</h3>
|
||||
</div>
|
||||
<div className="fs-1 opacity-50">
|
||||
<i className="bi bi-piggy-bank"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress mb-2" style={{ height: '8px', background: 'rgba(255,255,255,0.2)' }}>
|
||||
<div
|
||||
className="progress-bar bg-white"
|
||||
role="progressbar"
|
||||
style={{ width: `${Math.min(savingsRate, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<small className="opacity-75">
|
||||
{savingsRate >= 20 ? '🎯 ' + t('reports.excellentSavings') :
|
||||
savingsRate >= 10 ? '👍 ' + t('reports.goodSavings') :
|
||||
'💡 ' + t('reports.canImprove')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -235,12 +323,17 @@ const Reports = () => {
|
||||
<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 me-2"></i>
|
||||
{t('reports.yearComparison')}
|
||||
</h6>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h6 className="text-white mb-0">
|
||||
<i className="bi bi-bar-chart-line me-2"></i>
|
||||
{t('reports.yearComparison')} - {year-1} vs {year}
|
||||
</h6>
|
||||
<span className="badge bg-primary">
|
||||
{t('reports.total')}: {currency(summary.current.balance, summary.currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body" style={{ height: '300px' }}>
|
||||
<div className="card-body" style={{ height: '320px' }}>
|
||||
<Bar
|
||||
data={{
|
||||
labels: [t('common.incomes'), t('common.expenses'), t('common.balance')],
|
||||
@ -248,20 +341,49 @@ const Reports = () => {
|
||||
{
|
||||
label: String(year - 1),
|
||||
data: [summary.previous.income, summary.previous.expense, summary.previous.balance],
|
||||
backgroundColor: 'rgba(148, 163, 184, 0.5)',
|
||||
borderColor: '#94a3b8',
|
||||
borderWidth: 1,
|
||||
backgroundColor: 'rgba(100, 116, 139, 0.6)',
|
||||
borderColor: '#64748b',
|
||||
borderWidth: 2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
{
|
||||
label: String(year),
|
||||
data: [summary.current.income, summary.current.expense, summary.current.balance],
|
||||
backgroundColor: ['rgba(16, 185, 129, 0.7)', 'rgba(239, 68, 68, 0.7)', 'rgba(59, 130, 246, 0.7)'],
|
||||
backgroundColor: ['rgba(16, 185, 129, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(59, 130, 246, 0.8)'],
|
||||
borderColor: ['#10b981', '#ef4444', '#3b82f6'],
|
||||
borderWidth: 1,
|
||||
borderWidth: 2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={chartOptions}
|
||||
options={{
|
||||
...chartOptions,
|
||||
plugins: {
|
||||
...chartOptions.plugins,
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: '#94a3b8',
|
||||
padding: 15,
|
||||
font: { size: 12, weight: 'bold' }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#94a3b8',
|
||||
borderColor: '#334155',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `${context.dataset.label}: ${currency(context.parsed.y, summary.currency)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user