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:
parent
3c579afc66
commit
d03565d4ab
12
CHANGELOG.md
12
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user