v1.35.0: Redesign completo da seção de Comparação de Períodos

Improved:
- Cards KPI com gradientes e ícones para cada métrica
- Tabela comparativa detalhada lado a lado
- Gráfico de barras agrupadas profissional
- Layout responsivo otimizado
- Badges de variação com validação NaN
- Traduções completas (pt-BR, en, es)
This commit is contained in:
marcoitaloesp-ai 2025-12-15 16:06:16 +00:00 committed by GitHub
parent 3c579afc66
commit d03565d4ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 249 additions and 59 deletions

View File

@ -5,6 +5,18 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.35.0] - 2025-12-15
### Improved
- **UI/UX da Comparação de Períodos** - Interface completamente redesenhada
- Cards KPI com gradientes e ícones grandes para cada métrica
- Badges de variação com validação de NaN/Infinity
- Tabela comparativa detalhada com métricas lado a lado
- Gráfico de barras agrupadas com tooltips aprimorados
- Layout responsivo: tabela 5 colunas + gráfico 7 colunas
- Design profissional com hierarquia visual clara
- Traduções: periodComparison, vsPreviousPeriod, detailedComparison, visualComparison, metric, variation
## [1.34.7] - 2025-12-15
### Fixed

View File

@ -1 +1 @@
1.34.7
1.35.0

View File

@ -1801,6 +1801,12 @@
"byCategory": "By Category",
"byCostCenter": "By Cost Center",
"comparison": "Comparison",
"periodComparison": "Period Comparison",
"vsPreviousPeriod": "vs previous period",
"detailedComparison": "Detailed Comparison",
"visualComparison": "Visual Comparison",
"metric": "Metric",
"variation": "Variation",
"custom": "Custom",
"dayOfWeek": {
"friday": "Friday",

View File

@ -1783,6 +1783,12 @@
"byCostCenter": "Por Centro de Costo",
"monthlyEvolution": "Evolución Mensual",
"comparison": "Comparativa",
"periodComparison": "Comparación de Períodos",
"vsPreviousPeriod": "vs período anterior",
"detailedComparison": "Comparación Detallada",
"visualComparison": "Comparación Visual",
"metric": "Métrica",
"variation": "Variación",
"topExpenses": "Mayores Gastos",
"projection": "Proyección",
"recurring": "Recurrentes",

View File

@ -1803,6 +1803,12 @@
"byCategory": "Por Categoria",
"byCostCenter": "Por Centro de Custo",
"comparison": "Comparação",
"periodComparison": "Comparação de Períodos",
"vsPreviousPeriod": "vs período anterior",
"detailedComparison": "Comparação Detalhada",
"visualComparison": "Comparação Visual",
"metric": "Métrica",
"variation": "Variação",
"custom": "Personalizado",
"dayOfWeek": {
"friday": "Sexta",

View File

@ -614,75 +614,204 @@ const Reports = () => {
return (
<div className="row g-4">
<div className="col-md-6">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">{comparison.period2.label}</h6>
</div>
<div className="card-body">
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.income')}</span>
<span className="text-success">{currency(comparison.period2.income, comparison.currency)}</span>
{/* Header com seletor de períodos */}
<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-arrow-left-right me-2"></i>
{t('reports.periodComparison')}
</h5>
</div>
</div>
{/* Cards KPI Comparativos */}
<div className="col-lg-4 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">
<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(comparison.period1.income, comparison.currency)}</h3>
</div>
<div className="fs-1 opacity-50">
<i className="bi bi-arrow-up-circle"></i>
</div>
</div>
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.expenses')}</span>
<span className="text-danger">{currency(comparison.period2.expense, comparison.currency)}</span>
<div className="d-flex align-items-center gap-2 mb-2">
{comparison.variation.income !== 0 && !isNaN(comparison.variation.income) && isFinite(comparison.variation.income) && (
<>
<span className={`badge ${comparison.variation.income >= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
<i className={`bi bi-arrow-${comparison.variation.income >= 0 ? 'up' : 'down'} me-1`}></i>
{comparison.variation.income > 0 ? '+' : ''}{Math.abs(comparison.variation.income).toFixed(1)}%
</span>
<small className="opacity-75">{t('reports.vsPreviousPeriod')}</small>
</>
)}
</div>
<div className="d-flex justify-content-between">
<span className="text-slate-400">{t('reports.balance')}</span>
<span className={comparison.period2.balance >= 0 ? 'text-success' : 'text-danger'}>
{currency(comparison.period2.balance, comparison.currency)}
</span>
<hr className="my-2 opacity-25" />
<div className="small">
<div className="d-flex justify-content-between opacity-75">
<span>{comparison.period2.label}:</span>
<span className="fw-semibold">{currency(comparison.period2.income, comparison.currency)}</span>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card border-0" style={{ background: '#0f172a', border: '2px solid #3b82f6' }}>
<div className="col-lg-4 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">
<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(comparison.period1.expense, comparison.currency)}</h3>
</div>
<div className="fs-1 opacity-50">
<i className="bi bi-arrow-down-circle"></i>
</div>
</div>
<div className="d-flex align-items-center gap-2 mb-2">
{comparison.variation.expense !== 0 && !isNaN(comparison.variation.expense) && isFinite(comparison.variation.expense) && (
<>
<span className={`badge ${comparison.variation.expense <= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
<i className={`bi bi-arrow-${comparison.variation.expense >= 0 ? 'up' : 'down'} me-1`}></i>
{comparison.variation.expense > 0 ? '+' : ''}{Math.abs(comparison.variation.expense).toFixed(1)}%
</span>
<small className="opacity-75">{t('reports.vsPreviousPeriod')}</small>
</>
)}
</div>
<hr className="my-2 opacity-25" />
<div className="small">
<div className="d-flex justify-content-between opacity-75">
<span>{comparison.period2.label}:</span>
<span className="fw-semibold">{currency(comparison.period2.expense, comparison.currency)}</span>
</div>
</div>
</div>
</div>
</div>
<div className="col-lg-4 col-md-12">
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 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.balance')}</div>
<h3 className="mb-0 fw-bold">{currency(comparison.period1.balance, comparison.currency)}</h3>
</div>
<div className="fs-1 opacity-50">
<i className="bi bi-wallet2"></i>
</div>
</div>
<div className="d-flex align-items-center gap-2 mb-2">
{comparison.variation.balance !== 0 && !isNaN(comparison.variation.balance) && isFinite(comparison.variation.balance) && (
<>
<span className={`badge ${comparison.variation.balance >= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
<i className={`bi bi-arrow-${comparison.variation.balance >= 0 ? 'up' : 'down'} me-1`}></i>
{comparison.variation.balance > 0 ? '+' : ''}{Math.abs(comparison.variation.balance).toFixed(1)}%
</span>
<small className="opacity-75">{t('reports.vsPreviousPeriod')}</small>
</>
)}
</div>
<hr className="my-2 opacity-25" />
<div className="small">
<div className="d-flex justify-content-between opacity-75">
<span>{comparison.period2.label}:</span>
<span className="fw-semibold">{currency(comparison.period2.balance, comparison.currency)}</span>
</div>
</div>
</div>
</div>
</div>
{/* Tabela Comparativa Detalhada */}
<div className="col-lg-5">
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
{comparison.period1.label}
<span className="badge bg-primary ms-2">{t('common.current')}</span>
<i className="bi bi-table me-2"></i>
{t('reports.detailedComparison')}
</h6>
</div>
<div className="card-body">
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.income')}</span>
<div>
<span className="text-success">{currency(comparison.period1.income, comparison.currency)}</span>
{comparison.variation.income !== 0 && (
<span className={`badge ms-2 ${comparison.variation.income >= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.income > 0 ? '+' : ''}{comparison.variation.income}%
</span>
)}
</div>
</div>
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.expenses')}</span>
<div>
<span className="text-danger">{currency(comparison.period1.expense, comparison.currency)}</span>
{comparison.variation.expense !== 0 && (
<span className={`badge ms-2 ${comparison.variation.expense <= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.expense > 0 ? '+' : ''}{comparison.variation.expense}%
</span>
)}
</div>
</div>
<div className="d-flex justify-content-between">
<span className="text-slate-400">{t('reports.balance')}</span>
<span className={comparison.period1.balance >= 0 ? 'text-success' : 'text-danger'}>
{currency(comparison.period1.balance, comparison.currency)}
</span>
</div>
<table className="table table-dark table-borderless mb-0">
<thead className="border-bottom border-secondary">
<tr>
<th className="text-slate-400 fw-normal small">{t('reports.metric')}</th>
<th className="text-end text-slate-400 fw-normal small">{comparison.period2.label}</th>
<th className="text-end text-slate-400 fw-normal small">{comparison.period1.label}</th>
<th className="text-end text-slate-400 fw-normal small">{t('reports.variation')}</th>
</tr>
</thead>
<tbody>
<tr>
<td className="text-white">{t('reports.income')}</td>
<td className="text-end text-success">{currency(comparison.period2.income, comparison.currency)}</td>
<td className="text-end text-success fw-bold">{currency(comparison.period1.income, comparison.currency)}</td>
<td className="text-end">
{comparison.variation.income !== 0 && !isNaN(comparison.variation.income) && isFinite(comparison.variation.income) && (
<span className={`badge ${comparison.variation.income >= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.income > 0 ? '+' : ''}{comparison.variation.income.toFixed(1)}%
</span>
)}
</td>
</tr>
<tr>
<td className="text-white">{t('reports.expenses')}</td>
<td className="text-end text-danger">{currency(comparison.period2.expense, comparison.currency)}</td>
<td className="text-end text-danger fw-bold">{currency(comparison.period1.expense, comparison.currency)}</td>
<td className="text-end">
{comparison.variation.expense !== 0 && !isNaN(comparison.variation.expense) && isFinite(comparison.variation.expense) && (
<span className={`badge ${comparison.variation.expense <= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.expense > 0 ? '+' : ''}{comparison.variation.expense.toFixed(1)}%
</span>
)}
</td>
</tr>
<tr className="border-top border-secondary">
<td className="text-white fw-bold">{t('reports.balance')}</td>
<td className={`text-end fw-bold ${comparison.period2.balance >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(comparison.period2.balance, comparison.currency)}
</td>
<td className={`text-end fw-bold ${comparison.period1.balance >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(comparison.period1.balance, comparison.currency)}
</td>
<td className="text-end">
{comparison.variation.balance !== 0 && !isNaN(comparison.variation.balance) && isFinite(comparison.variation.balance) && (
<span className={`badge ${comparison.variation.balance >= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.balance > 0 ? '+' : ''}{comparison.variation.balance.toFixed(1)}%
</span>
)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/* Comparison Chart */}
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-body" style={{ height: '300px' }}>
{/* Gráfico de Barras Agrupadas */}
<div className="col-lg-7">
<div className="card border-0 h-100" 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.visualComparison')}
</h6>
</div>
<div className="card-body" style={{ height: '320px' }}>
<Bar
data={{
labels: [t('reports.income'), t('reports.expenses'), t('reports.balance')],
@ -690,18 +819,49 @@ const Reports = () => {
{
label: comparison.period2.label,
data: [comparison.period2.income, comparison.period2.expense, comparison.period2.balance],
backgroundColor: 'rgba(148, 163, 184, 0.5)',
borderRadius: 4,
backgroundColor: 'rgba(100, 116, 139, 0.6)',
borderColor: '#64748b',
borderWidth: 2,
borderRadius: 6,
},
{
label: comparison.period1.label,
data: [comparison.period1.income, comparison.period1.expense, comparison.period1.balance],
backgroundColor: ['rgba(16, 185, 129, 0.7)', 'rgba(239, 68, 68, 0.7)', 'rgba(59, 130, 246, 0.7)'],
borderRadius: 4,
backgroundColor: ['rgba(16, 185, 129, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(59, 130, 246, 0.8)'],
borderColor: ['#10b981', '#ef4444', '#3b82f6'],
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, comparison.currency)}`;
}
}
}
}
}}
/>
</div>
</div>