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/). 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 ## [1.34.7] - 2025-12-15
### Fixed ### Fixed

View File

@ -1 +1 @@
1.34.7 1.35.0

View File

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

View File

@ -1783,6 +1783,12 @@
"byCostCenter": "Por Centro de Costo", "byCostCenter": "Por Centro de Costo",
"monthlyEvolution": "Evolución Mensual", "monthlyEvolution": "Evolución Mensual",
"comparison": "Comparativa", "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", "topExpenses": "Mayores Gastos",
"projection": "Proyección", "projection": "Proyección",
"recurring": "Recurrentes", "recurring": "Recurrentes",

View File

@ -1803,6 +1803,12 @@
"byCategory": "Por Categoria", "byCategory": "Por Categoria",
"byCostCenter": "Por Centro de Custo", "byCostCenter": "Por Centro de Custo",
"comparison": "Comparação", "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", "custom": "Personalizado",
"dayOfWeek": { "dayOfWeek": {
"friday": "Sexta", "friday": "Sexta",

View File

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