v1.43.0 - Mobile UX: Widgets Projeção e Sobrepagamentos otimizados

This commit is contained in:
marcoitaloesp-ai 2025-12-16 10:59:25 +00:00 committed by GitHub
parent bc47071fd8
commit e753c65cf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 330 additions and 152 deletions

View File

@ -5,6 +5,52 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.43.0] - 2025-12-16
### Added
- **Widget Projeção de Saldo Mobile** - Comportamento colapsável implementado
- Inicia colapsado em mobile, expande automaticamente se houver dados
- Botão toggle chevron up/down no header
- Botões de período (1, 2, 3, 6, 12 meses) otimizados:
- Desktop: btn-group horizontal
- Mobile: layout flex-wrap em 3 colunas (33.333% cada)
- Altura do gráfico ajustada: 300px (mobile) vs 350px (desktop)
- Cards de estatísticas otimizados para mobile:
- Padding reduzido: p-2 (mobile) vs p-3 (desktop)
- Gap menor: g-2 vs g-3
- Fontes menores: labels 0.7rem, valores 0.9rem
- Percentual de variação oculto em mobile
- **Widget Análise de Sobrepagamentos Mobile** - UX completamente redesenhada
- Comportamento colapsável com auto-expansão quando há dados
- Header otimizado: texto menor, badge compacto, "(12 meses)" oculto
- Gráfico responsivo: 180px (mobile) vs 220px (desktop)
- Cards de resumo compactos: padding p-1, fontes 0.65-0.85rem
- **Layout de transações revolucionário**:
- Mobile: Cards verticais ao invés de tabela horizontal
- Cada card mostra: descrição, valor (+), categoria, data, percentual
- Fontes otimizadas: 0.6-0.75rem
- Border amarelo sutil para destaque
- Altura máxima: 200px com scroll
- Desktop: Tabela completa mantida com todas as colunas
### Fixed
- **React Hooks Error #310** - Corrigida violação das regras de hooks
- Problema: `useEffect` sendo chamado após returns condicionais
- Solução: Movido cálculo de `monthsData` para antes dos returns
- Adicionado `useEffect` na importação: `import { useEffect }`
- Todos os hooks agora executam na ordem correta
- Dependências corretas em useEffect: `[isMobile, monthsData.length, isExpanded]`
### Improved
- **Consistência Mobile Global** - Todos os widgets principais otimizados:
- ✅ Calendário: navegação entre semanas + colapso
- ✅ Próximos 7 Dias: colapso + sync altura desktop
- ✅ Transações em Atraso: colapso + auto-expansão
- ✅ Projeção de Saldo: colapso + botões otimizados + cards compactos
- ✅ Análise Sobrepagamentos: colapso + layout cards mobile
- UX mobile profissional e consistente em todo Dashboard
## [1.42.0] - 2025-12-16
### Added

View File

@ -1 +1 @@
1.42.0
1.43.0

View File

@ -35,6 +35,8 @@ const BalanceProjectionChart = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [isExpanded, setIsExpanded] = useState(false);
const loadData = useCallback(async () => {
setLoading(true);
@ -42,18 +44,36 @@ const BalanceProjectionChart = () => {
try {
const response = await reportService.getProjectionChart({ months });
setData(response);
// Auto-expand if we have data points in mobile
if (isMobile && response?.data?.length > 0) {
setIsExpanded(true);
}
} catch (err) {
console.error('Error loading projection chart:', err);
setError(t('common.error'));
} finally {
setLoading(false);
}
}, [months, t]);
}, [months, t, isMobile]);
useEffect(() => {
loadData();
}, [loadData]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
setIsMobile(mobile);
if (!mobile) {
setIsExpanded(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const periodOptions = [
{ value: 1, label: t('reports.projectionChart.1month') || '1 mês' },
{ value: 2, label: t('reports.projectionChart.2months') || '2 meses' },
@ -202,30 +222,44 @@ const BalanceProjectionChart = () => {
const changeIcon = summary?.change >= 0 ? 'bi-arrow-up' : 'bi-arrow-down';
return (
<div className="card border-0 shadow-sm bg-slate-800">
<div className="card-header bg-transparent border-bottom border-slate-700 d-flex justify-content-between align-items-center">
<div>
<h5 className="card-title text-light mb-1">
<i className="bi bi-graph-up-arrow me-2 text-primary"></i>
{t('reports.projectionChart.title') || 'Projeção de Saldo'}
</h5>
<small className="text-slate-400">
{t('reports.projectionChart.subtitle') || 'Evolução prevista do seu saldo'}
</small>
</div>
<div className="btn-group">
{periodOptions.map(opt => (
<div className="card border-0 shadow-sm bg-slate-800" style={{ height: isMobile ? 'auto' : undefined }}>
<div className="card-header bg-transparent border-bottom border-slate-700">
<div className="d-flex justify-content-between align-items-center mb-2">
<div className="flex-grow-1">
<h5 className="card-title text-light mb-1">
<i className="bi bi-graph-up-arrow me-2 text-primary"></i>
{t('reports.projectionChart.title') || 'Projeção de Saldo'}
</h5>
<small className="text-slate-400">
{t('reports.projectionChart.subtitle') || 'Evolução prevista do seu saldo'}
</small>
</div>
{isMobile && (
<button
key={opt.value}
className={`btn btn-sm ${months === opt.value ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setMonths(opt.value)}
className="btn btn-sm btn-outline-light ms-2"
onClick={() => setIsExpanded(!isExpanded)}
aria-label={isExpanded ? "Colapsar" : "Expandir"}
>
{opt.label}
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
</button>
))}
)}
</div>
{(!isMobile || isExpanded) && (
<div className={isMobile ? "d-flex flex-wrap gap-2" : "d-flex justify-content-end"}>
{periodOptions.map(opt => (
<button
key={opt.value}
className={`btn btn-sm ${months === opt.value ? 'btn-primary' : 'btn-outline-secondary'} ${isMobile ? 'flex-fill' : ''}`}
onClick={() => setMonths(opt.value)}
style={isMobile ? { minWidth: 'calc(33.333% - 0.5rem)' } : undefined}
>
{opt.label}
</button>
))}
</div>
)}
</div>
<div className="card-body">
<div className="card-body" style={{ display: isMobile && !isExpanded ? 'none' : 'block' }}>
{/* Summary Stats */}
{summary && (
<>
@ -245,36 +279,46 @@ const BalanceProjectionChart = () => {
</div>
)}
<div className="row g-3 mb-4">
<div className="row g-2 mb-3">
<div className="col-6 col-md-3">
<div className="p-3 rounded bg-slate-700">
<small className="text-slate-400 d-block">{t('reports.projectionChart.currentBalance') || 'Saldo Atual'}</small>
<span className="fs-5 fw-bold text-light">{currency(summary.current_balance, data?.currency)}</span>
<div className={`${isMobile ? 'p-2' : 'p-3'} rounded bg-slate-700`}>
<small className={`text-slate-400 d-block ${isMobile ? 'mb-0' : ''}`} style={isMobile ? { fontSize: '0.7rem' } : undefined}>
{t('reports.projectionChart.currentBalance') || 'Saldo Atual'}
</small>
<span className={`${isMobile ? 'fs-6' : 'fs-5'} fw-bold text-light d-block`} style={isMobile ? { fontSize: '0.9rem' } : undefined}>
{currency(summary.current_balance, data?.currency)}
</span>
</div>
</div>
<div className="col-6 col-md-3">
<div className="p-3 rounded bg-slate-700">
<small className="text-slate-400 d-block">{t('reports.projectionChart.finalBalance') || 'Saldo Final'}</small>
<span className={`fs-5 fw-bold ${summary.final_balance >= 0 ? 'text-success' : 'text-danger'}`}>
<div className={`${isMobile ? 'p-2' : 'p-3'} rounded bg-slate-700`}>
<small className={`text-slate-400 d-block ${isMobile ? 'mb-0' : ''}`} style={isMobile ? { fontSize: '0.7rem' } : undefined}>
{t('reports.projectionChart.finalBalance') || 'Saldo Final'}
</small>
<span className={`${isMobile ? 'fs-6' : 'fs-5'} fw-bold ${summary.final_balance >= 0 ? 'text-success' : 'text-danger'} d-block`} style={isMobile ? { fontSize: '0.9rem' } : undefined}>
{currency(summary.final_balance, data?.currency)}
</span>
</div>
</div>
<div className="col-6 col-md-3">
<div className="p-3 rounded bg-slate-700">
<small className="text-slate-400 d-block">{t('reports.projectionChart.minBalance') || 'Saldo Mínimo'}</small>
<span className={`fs-5 fw-bold ${summary.min_balance >= 0 ? 'text-warning' : 'text-danger'}`}>
<div className={`${isMobile ? 'p-2' : 'p-3'} rounded bg-slate-700`}>
<small className={`text-slate-400 d-block ${isMobile ? 'mb-0' : ''}`} style={isMobile ? { fontSize: '0.7rem' } : undefined}>
{t('reports.projectionChart.minBalance') || 'Saldo Mínimo'}
</small>
<span className={`${isMobile ? 'fs-6' : 'fs-5'} fw-bold ${summary.min_balance >= 0 ? 'text-warning' : 'text-danger'} d-block`} style={isMobile ? { fontSize: '0.9rem' } : undefined}>
{currency(summary.min_balance, data?.currency)}
</span>
</div>
</div>
<div className="col-6 col-md-3">
<div className="p-3 rounded bg-slate-700">
<small className="text-slate-400 d-block">{t('reports.projectionChart.change') || 'Variação'}</small>
<span className={`fs-5 fw-bold ${changeClass}`}>
<div className={`${isMobile ? 'p-2' : 'p-3'} rounded bg-slate-700`}>
<small className={`text-slate-400 d-block ${isMobile ? 'mb-0' : ''}`} style={isMobile ? { fontSize: '0.7rem' } : undefined}>
{t('reports.projectionChart.change') || 'Variação'}
</small>
<span className={`${isMobile ? 'fs-6' : 'fs-5'} fw-bold ${changeClass} d-block`} style={isMobile ? { fontSize: '0.9rem' } : undefined}>
<i className={`bi ${changeIcon} me-1`}></i>
{currency(Math.abs(summary.change), data?.currency)}
<small className="ms-1">({summary.change_percent}%)</small>
{!isMobile && <small className="ms-1">({summary.change_percent}%)</small>}
</span>
</div>
</div>
@ -295,7 +339,7 @@ const BalanceProjectionChart = () => {
)}
{/* Chart */}
<div style={{ height: '350px' }}>
<div style={{ height: isMobile ? '300px' : '350px' }}>
<Line data={chartData} options={options} />
</div>

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import {
Chart as ChartJS,
CategoryScale,
@ -29,10 +29,56 @@ const OverpaymentsAnalysis = ({ data, loading, onTransactionClick }) => {
const { currency } = useFormatters();
const chartRef = useRef(null);
const [selectedMonth, setSelectedMonth] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [isExpanded, setIsExpanded] = useState(false);
// Helper para locale dinâmico
const getLocale = () => i18n.language === 'pt-BR' ? 'pt-BR' : i18n.language === 'es' ? 'es-ES' : 'en-US';
// Agrupar transações por mês (apenas sobrepagamentos - variance > 0)
const overpaymentsByMonth = {};
if (data?.transactions) {
data.transactions
.filter(t => t.variance > 0)
.forEach(t => {
const month = t.effective_date.substring(0, 7);
if (!overpaymentsByMonth[month]) {
overpaymentsByMonth[month] = {
month,
total: 0,
count: 0,
transactions: []
};
}
overpaymentsByMonth[month].total += t.variance;
overpaymentsByMonth[month].count += 1;
overpaymentsByMonth[month].transactions.push(t);
});
}
// Criar array de meses ordenados
const monthsData = Object.values(overpaymentsByMonth).sort((a, b) => a.month.localeCompare(b.month));
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
setIsMobile(mobile);
if (!mobile) {
setIsExpanded(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Auto-expand if we have data in mobile
useEffect(() => {
if (isMobile && monthsData.length > 0 && !isExpanded) {
setIsExpanded(true);
}
}, [isMobile, monthsData.length, isExpanded]);
if (loading) {
return (
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
@ -65,28 +111,6 @@ const OverpaymentsAnalysis = ({ data, loading, onTransactionClick }) => {
);
}
// Agrupar transações por mês (apenas sobrepagamentos - variance > 0)
const overpaymentsByMonth = {};
data.transactions
.filter(t => t.variance > 0)
.forEach(t => {
const month = t.effective_date.substring(0, 7);
if (!overpaymentsByMonth[month]) {
overpaymentsByMonth[month] = {
month,
total: 0,
count: 0,
transactions: []
};
}
overpaymentsByMonth[month].total += t.variance;
overpaymentsByMonth[month].count += 1;
overpaymentsByMonth[month].transactions.push(t);
});
// Criar array de meses ordenados
const monthsData = Object.values(overpaymentsByMonth).sort((a, b) => a.month.localeCompare(b.month));
// Calcular total geral
const totalOverpayment = monthsData.reduce((sum, m) => sum + m.total, 0);
@ -180,29 +204,44 @@ const OverpaymentsAnalysis = ({ data, loading, onTransactionClick }) => {
const selectedMonthData = selectedMonth !== null ? monthsData[selectedMonth] : null;
return (
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
<div className="card border-0 h-100" style={{ background: '#0f172a', height: isMobile ? 'auto' : undefined }}>
{/* Header */}
<div className="card-header border-0 d-flex justify-content-between align-items-center py-3"
<div className="card-header border-0 py-3"
style={{ background: 'transparent', borderBottom: '1px solid rgba(251, 191, 36, 0.2)' }}>
<h6 className="text-white mb-0 d-flex align-items-center">
<i className="bi bi-graph-up-arrow me-2 text-warning"></i>
{t('dashboard.overpaymentsAnalysis')} ({data.period?.months || 12} {t('common.months')})
</h6>
<span className="badge bg-warning text-dark px-3 py-2">
<i className="bi bi-arrow-up-right me-1"></i>
Total: {currency(totalOverpayment, 'BRL')}
</span>
<div className="d-flex justify-content-between align-items-center mb-2">
<h6 className="text-white mb-0 d-flex align-items-center flex-grow-1">
<i className="bi bi-graph-up-arrow me-2 text-warning"></i>
<span className={isMobile ? 'small' : ''}>
{t('dashboard.overpaymentsAnalysis')} {!isMobile && `(${data.period?.months || 12} ${t('common.months')})`}
</span>
</h6>
{isMobile && (
<button
className="btn btn-sm btn-outline-warning ms-2"
onClick={() => setIsExpanded(!isExpanded)}
aria-label={isExpanded ? "Colapsar" : "Expandir"}
>
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
</button>
)}
</div>
{(!isMobile || isExpanded) && (
<span className={`badge bg-warning text-dark ${isMobile ? 'px-2 py-1' : 'px-3 py-2'}`}>
<i className="bi bi-arrow-up-right me-1"></i>
Total: {currency(totalOverpayment, 'BRL')}
</span>
)}
</div>
<div className="card-body">
<div className="card-body" style={{ display: isMobile && !isExpanded ? 'none' : 'block' }}>
{/* Instrução */}
<p className="text-slate-500 small mb-3">
<p className={`text-slate-500 ${isMobile ? 'mb-2' : 'mb-3'}`} style={{ fontSize: isMobile ? '0.75rem' : undefined }}>
<i className="bi bi-info-circle me-1"></i>
{t('dashboard.clickBarToSeeDetails')}
</p>
{/* Gráfico */}
<div style={{ height: '220px' }}>
<div style={{ height: isMobile ? '180px' : '220px' }}>
<Bar ref={chartRef} data={chartData} options={options} />
</div>
@ -231,33 +270,33 @@ const OverpaymentsAnalysis = ({ data, loading, onTransactionClick }) => {
</div>
{/* Resumo do mês */}
<div className="row g-3 mb-3">
<div className={`row ${isMobile ? 'g-2' : 'g-3'} mb-3`}>
<div className="col-4">
<div className="text-center p-2 rounded" style={{ background: 'rgba(0,0,0,0.2)' }}>
<small className="text-slate-400 d-block text-uppercase" style={{ fontSize: '10px' }}>
<div className={`text-center ${isMobile ? 'p-1' : 'p-2'} rounded`} style={{ background: 'rgba(0,0,0,0.2)' }}>
<small className="text-slate-400 d-block text-uppercase" style={{ fontSize: isMobile ? '0.65rem' : '10px' }}>
{t('dashboard.totalOverpaid')}
</small>
<span className="text-warning fw-bold fs-5">
<span className={`text-warning fw-bold ${isMobile ? 'fs-6' : 'fs-5'}`} style={{ fontSize: isMobile ? '0.85rem' : undefined }}>
+{currency(selectedMonthData.total, 'BRL')}
</span>
</div>
</div>
<div className="col-4">
<div className="text-center p-2 rounded" style={{ background: 'rgba(0,0,0,0.2)' }}>
<small className="text-slate-400 d-block text-uppercase" style={{ fontSize: '10px' }}>
<div className={`text-center ${isMobile ? 'p-1' : 'p-2'} rounded`} style={{ background: 'rgba(0,0,0,0.2)' }}>
<small className="text-slate-400 d-block text-uppercase" style={{ fontSize: isMobile ? '0.65rem' : '10px' }}>
{t('dashboard.transactions')}
</small>
<span className="text-white fw-bold fs-5">
<span className={`text-white fw-bold ${isMobile ? 'fs-6' : 'fs-5'}`} style={{ fontSize: isMobile ? '0.85rem' : undefined }}>
{selectedMonthData.count}
</span>
</div>
</div>
<div className="col-4">
<div className="text-center p-2 rounded" style={{ background: 'rgba(0,0,0,0.2)' }}>
<small className="text-slate-400 d-block text-uppercase" style={{ fontSize: '10px' }}>
<div className={`text-center ${isMobile ? 'p-1' : 'p-2'} rounded`} style={{ background: 'rgba(0,0,0,0.2)' }}>
<small className="text-slate-400 d-block text-uppercase" style={{ fontSize: isMobile ? '0.65rem' : '10px' }}>
{t('dashboard.avgPerTransaction')}
</small>
<span className="text-warning fw-bold fs-5">
<span className={`text-warning fw-bold ${isMobile ? 'fs-6' : 'fs-5'}`} style={{ fontSize: isMobile ? '0.85rem' : undefined }}>
+{currency(selectedMonthData.total / selectedMonthData.count, 'BRL')}
</span>
</div>
@ -265,87 +304,136 @@ const OverpaymentsAnalysis = ({ data, loading, onTransactionClick }) => {
</div>
{/* Tabela de transações */}
<div className="table-responsive" style={{ maxHeight: '200px', overflowY: 'auto' }}>
<table className="table table-sm mb-0" style={{ '--bs-table-bg': 'transparent' }}>
<thead style={{ position: 'sticky', top: 0, background: '#0f172a' }}>
<tr>
<th className="text-slate-400 small border-0 text-uppercase" style={{ fontSize: '10px' }}>{t('common.date')}</th>
<th className="text-slate-400 small border-0 text-uppercase" style={{ fontSize: '10px' }}>{t('common.description')}</th>
<th className="text-slate-400 small border-0 text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.category')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.plannedValue')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.actualValue')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.difference')}</th>
<th className="text-slate-400 small border-0 text-center text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.delay')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>%</th>
</tr>
</thead>
<tbody>
{selectedMonthData.transactions.map((tx) => (
<tr
key={tx.id}
className="cursor-pointer"
onClick={() => onTransactionClick?.(tx.id)}
style={{ cursor: 'pointer' }}
>
<td className="text-slate-300 small border-0">
{new Date(tx.effective_date).toLocaleDateString(getLocale())}
</td>
<td className="border-0">
<span className="text-white small">
{tx.description.length > 30 ? tx.description.substring(0, 30) + '...' : tx.description}
</span>
</td>
<td className="border-0">
{tx.category ? (
{isMobile ? (
/* Layout Mobile - Cards */
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
{selectedMonthData.transactions.map((tx) => (
<div
key={tx.id}
className="p-2 mb-2 rounded"
onClick={() => onTransactionClick?.(tx.id)}
style={{
cursor: 'pointer',
background: 'rgba(0,0,0,0.3)',
border: '1px solid rgba(251, 191, 36, 0.2)'
}}
>
<div className="d-flex justify-content-between align-items-start mb-1">
<span className="text-white" style={{ fontSize: '0.75rem', fontWeight: '500' }}>
{tx.description.length > 25 ? tx.description.substring(0, 25) + '...' : tx.description}
</span>
<span className="badge bg-warning text-dark ms-2" style={{ fontSize: '0.65rem' }}>
+{currency(tx.variance, 'BRL')}
</span>
</div>
<div className="d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
{tx.category && (
<span
className="badge rounded-pill small"
className="badge rounded-pill"
style={{
background: tx.category.color || '#6b7280',
fontSize: '10px'
fontSize: '0.6rem'
}}
>
{tx.category.name}
</span>
) : (
<span className="text-slate-500 small">-</span>
)}
</td>
<td className="text-slate-400 small text-end border-0">
{currency(tx.planned_amount, 'BRL')}
</td>
<td className="text-white small text-end border-0">
{currency(tx.actual_amount, 'BRL')}
</td>
<td className="text-end border-0">
<span className="badge bg-warning text-dark small">
<i className="bi bi-arrow-up me-1" style={{ fontSize: '10px' }}></i>
+{currency(tx.variance, 'BRL')}
<span className="text-slate-400" style={{ fontSize: '0.65rem' }}>
{new Date(tx.effective_date).toLocaleDateString(getLocale(), { day: '2-digit', month: '2-digit' })}
</span>
</td>
<td className="text-center border-0">
{tx.delay_days !== null && tx.delay_days !== 0 ? (
<span
className="badge small"
style={{
fontSize: '10px',
background: tx.delay_days > 0 ? 'rgba(239, 68, 68, 0.8)' : 'rgba(34, 197, 94, 0.8)'
}}
>
<i className={`bi bi-clock${tx.delay_days > 0 ? '-history' : ''} me-1`} style={{ fontSize: '9px' }}></i>
{tx.delay_days > 0 ? `+${tx.delay_days}d` : `${tx.delay_days}d`}
</span>
) : (
<span className="text-slate-500 small">-</span>
)}
</td>
<td className="text-warning small text-end border-0">
</div>
<span className="text-warning" style={{ fontSize: '0.65rem' }}>
+{tx.variance_percent}%
</td>
</span>
</div>
</div>
))}
</div>
) : (
/* Layout Desktop - Tabela */
<div className="table-responsive" style={{ maxHeight: '200px', overflowY: 'auto', overflowX: 'auto' }}>
<table className="table table-sm mb-0" style={{ '--bs-table-bg': 'transparent' }}>
<thead style={{ position: 'sticky', top: 0, background: '#0f172a' }}>
<tr>
<th className="text-slate-400 small border-0 text-uppercase" style={{ fontSize: '10px' }}>{t('common.date')}</th>
<th className="text-slate-400 small border-0 text-uppercase" style={{ fontSize: '10px' }}>{t('common.description')}</th>
<th className="text-slate-400 small border-0 text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.category')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.plannedValue')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.actualValue')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.difference')}</th>
<th className="text-slate-400 small border-0 text-center text-uppercase" style={{ fontSize: '10px' }}>{t('dashboard.delay')}</th>
<th className="text-slate-400 small border-0 text-end text-uppercase" style={{ fontSize: '10px' }}>%</th>
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{selectedMonthData.transactions.map((tx) => (
<tr
key={tx.id}
className="cursor-pointer"
onClick={() => onTransactionClick?.(tx.id)}
style={{ cursor: 'pointer' }}
>
<td className="text-slate-300 small border-0">
{new Date(tx.effective_date).toLocaleDateString(getLocale())}
</td>
<td className="border-0">
<span className="text-white small">
{tx.description.length > 30 ? tx.description.substring(0, 30) + '...' : tx.description}
</span>
</td>
<td className="border-0">
{tx.category ? (
<span
className="badge rounded-pill small"
style={{
background: tx.category.color || '#6b7280',
fontSize: '10px'
}}
>
{tx.category.name}
</span>
) : (
<span className="text-slate-500 small">-</span>
)}
</td>
<td className="text-slate-400 small text-end border-0">
{currency(tx.planned_amount, 'BRL')}
</td>
<td className="text-white small text-end border-0">
{currency(tx.actual_amount, 'BRL')}
</td>
<td className="text-end border-0">
<span className="badge bg-warning text-dark small">
<i className="bi bi-arrow-up me-1" style={{ fontSize: '10px' }}></i>
+{currency(tx.variance, 'BRL')}
</span>
</td>
<td className="text-center border-0">
{tx.delay_days !== null && tx.delay_days !== 0 ? (
<span
className="badge small"
style={{
fontSize: '10px',
background: tx.delay_days > 0 ? 'rgba(239, 68, 68, 0.8)' : 'rgba(34, 197, 94, 0.8)'
}}
>
<i className={`bi bi-clock${tx.delay_days > 0 ? '-history' : ''} me-1`} style={{ fontSize: '9px' }}></i>
{tx.delay_days > 0 ? `+${tx.delay_days}d` : `${tx.delay_days}d`}
</span>
) : (
<span className="text-slate-500 small">-</span>
)}
</td>
<td className="text-warning small text-end border-0">
+{tx.variance_percent}%
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}