v1.43.0 - Mobile UX: Widgets Projeção e Sobrepagamentos otimizados
This commit is contained in:
parent
bc47071fd8
commit
e753c65cf0
46
CHANGELOG.md
46
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user