webmoney/frontend/src/pages/Dashboard.jsx
2025-12-15 16:50:52 +00:00

470 lines
18 KiB
JavaScript

import React, { useState, useEffect, useCallback } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import { dashboardService, accountService } from '../services/api';
import useFormatters from '../hooks/useFormatters';
import CashflowChart from '../components/dashboard/CashflowChart';
import BalanceProjectionChart from '../components/dashboard/BalanceProjectionChart';
import OverpaymentsAnalysis from '../components/dashboard/OverpaymentsAnalysis';
import CalendarWidget from '../components/dashboard/CalendarWidget';
import UpcomingWidget from '../components/dashboard/UpcomingWidget';
import OverdueWidget from '../components/dashboard/OverdueWidget';
const Dashboard = () => {
const { user } = useAuth();
const { t, i18n } = useTranslation();
const { currency } = useFormatters();
const navigate = useNavigate();
// Estados
const [loading, setLoading] = useState(true);
const [cashflowLoading, setCashflowLoading] = useState(true);
const [variancesLoading, setVariancesLoading] = useState(true);
// Dados do dashboard
const [summary, setSummary] = useState(null);
const [cashflow, setCashflow] = useState(null);
const [variances, setVariances] = useState(null);
const [accounts, setAccounts] = useState([]);
// Filtros dos gráficos
const [cashflowMonths, setCashflowMonths] = useState(12);
const [variancesMonths, setVariancesMonths] = useState(12);
// Carregar resumo geral
const loadSummary = useCallback(async () => {
try {
const [summaryData, accountsData] = await Promise.all([
dashboardService.getSummary(),
accountService.getAll(),
]);
setSummary(summaryData);
setAccounts(accountsData.data || []);
} catch (error) {
console.error('Error loading summary:', error);
} finally {
setLoading(false);
}
}, []);
// Carregar fluxo de caixa
const loadCashflow = useCallback(async () => {
setCashflowLoading(true);
try {
const data = await dashboardService.getCashflow(cashflowMonths);
setCashflow(data);
} catch (error) {
console.error('Error loading cashflow:', error);
} finally {
setCashflowLoading(false);
}
}, [cashflowMonths]);
// Carregar variações de pagamento
const loadVariances = useCallback(async () => {
setVariancesLoading(true);
try {
const data = await dashboardService.getPaymentVariances(variancesMonths);
setVariances(data);
} catch (error) {
console.error('Error loading variances:', error);
} finally {
setVariancesLoading(false);
}
}, [variancesMonths]);
useEffect(() => {
loadSummary();
}, [loadSummary]);
useEffect(() => {
loadCashflow();
}, [loadCashflow]);
useEffect(() => {
loadVariances();
}, [loadVariances]);
// Handlers
const handleMonthsChange = (months) => {
setCashflowMonths(months);
};
const handleVariancesMonthsChange = (months) => {
setVariancesMonths(months);
};
const handleTransactionClick = (transactionId) => {
navigate(`/transactions?highlight=${transactionId}`);
};
// Componente de Stat Card modernizado
const StatCard = ({ icon, label, value, valueColor = 'text-white', trend = null, trendUp = true, subValue = null, accentColor = '#3b82f6' }) => (
<div className="card border-0 h-100 stat-card" style={{
background: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)',
borderLeft: `3px solid ${accentColor}`,
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}}>
<div className="card-body py-3">
<div className="d-flex justify-content-between align-items-start">
<div className="flex-grow-1">
<p className="text-slate-400 mb-1 small text-uppercase" style={{ fontSize: '11px', letterSpacing: '0.5px' }}>
{label}
</p>
{loading ? (
<div className="placeholder-glow">
<span className="placeholder col-8" style={{ height: '28px' }}></span>
</div>
) : (
<div className={`mb-0 fw-bold ${valueColor}`} style={{ fontSize: '1.25rem' }}>
{value}
</div>
)}
{subValue && !loading && (
<small className="text-slate-500">{subValue}</small>
)}
</div>
<div className="rounded-2 d-flex align-items-center justify-content-center flex-shrink-0"
style={{
width: '42px',
height: '42px',
background: `${accentColor}20`
}}>
<i className={`bi ${icon}`} style={{ color: accentColor, fontSize: '1.25rem' }}></i>
</div>
</div>
{trend !== null && !loading && (
<div className="mt-2">
<span className={`small ${trendUp ? 'text-success' : 'text-danger'}`}>
<i className={`bi bi-arrow-${trendUp ? 'up' : 'down'}-right me-1`}></i>
{trend}
</span>
<span className="text-slate-500 small ms-1">{t('dashboard.vsLastMonth')}</span>
</div>
)}
</div>
</div>
);
// Componente de conta
const AccountItem = ({ account }) => {
const balance = parseFloat(account.current_balance);
const isPositive = balance >= 0;
const getAccountIcon = (type) => {
const icons = {
'checking': 'bi-bank',
'savings': 'bi-piggy-bank',
'credit_card': 'bi-credit-card',
'investment': 'bi-graph-up-arrow',
'cash': 'bi-wallet2',
};
return icons[type] || 'bi-wallet2';
};
return (
<div className="d-flex align-items-center py-2 px-3 rounded-2 mb-2"
style={{ background: 'rgba(255,255,255,0.03)' }}>
<div className="rounded-circle d-flex align-items-center justify-content-center me-3"
style={{
width: '40px',
height: '40px',
background: 'rgba(59, 130, 246, 0.15)'
}}>
<i className={`bi ${getAccountIcon(account.type)} text-primary`}></i>
</div>
<div className="flex-grow-1">
<div className="text-white small fw-medium">{account.name}</div>
<div className="text-slate-500" style={{ fontSize: '11px' }}>
{account.bank_name || t(`accounts.types.${account.type}`)}
</div>
</div>
<div className="text-end">
<div className={`fw-bold ${isPositive ? 'text-success' : 'text-danger'}`}>
{currency(balance, account.currency || 'BRL')}
</div>
{!account.include_in_total && (
<small className="text-slate-500" style={{ fontSize: '10px' }}>
<i className="bi bi-eye-slash me-1"></i>
{t('accounts.notInTotal')}
</small>
)}
</div>
</div>
);
};
return (
<div className="dashboard-container">
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 className="text-white mb-1 fw-bold">
{t('dashboard.welcome')}, {user?.name?.split(' ')[0]}!
<span className="ms-2" style={{ fontSize: '1.5rem' }}>👋</span>
</h4>
<p className="text-slate-400 mb-0 small">
{new Date().toLocaleDateString(i18n.language === 'pt-BR' ? 'pt-BR' : i18n.language === 'es' ? 'es-ES' : 'en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</div>
<div className="d-flex gap-2">
<Link to="/transactions" className="btn btn-outline-light btn-sm">
<i className="bi bi-list-ul me-1"></i>
{t('nav.transactions')}
</Link>
<Link to="/import" className="btn btn-primary btn-sm">
<i className="bi bi-upload me-1"></i>
{t('nav.import')}
</Link>
</div>
</div>
{/* Stats Cards Row - Multi-currency */}
<div className="row g-3 mb-4">
<div className="col-6 col-lg-3">
<StatCard
icon="bi-wallet2"
label={t('dashboard.totalBalance')}
value={
summary?.balances_by_currency && Object.keys(summary.balances_by_currency).length > 0
? Object.entries(summary.balances_by_currency).map(([curr, val]) => (
<div key={curr}>{currency(val, curr)}</div>
))
: currency(0, 'BRL')
}
valueColor={
summary?.balances_by_currency
? Object.values(summary.balances_by_currency).reduce((a, b) => a + b, 0) >= 0
? 'text-success'
: 'text-danger'
: 'text-success'
}
accentColor={
summary?.balances_by_currency
? Object.values(summary.balances_by_currency).reduce((a, b) => a + b, 0) >= 0
? '#22c55e'
: '#ef4444'
: '#22c55e'
}
/>
</div>
<div className="col-6 col-lg-3">
<StatCard
icon="bi-arrow-up-circle"
label={t('dashboard.monthIncome')}
value={
summary?.current_month_by_currency?.income && Object.keys(summary.current_month_by_currency.income).length > 0
? Object.entries(summary.current_month_by_currency.income).map(([curr, val]) => (
<div key={curr}>{currency(val, curr)}</div>
))
: currency(0, summary?.primary_currency || 'BRL')
}
valueColor="text-success"
accentColor="#22c55e"
/>
</div>
<div className="col-6 col-lg-3">
<StatCard
icon="bi-arrow-down-circle"
label={t('dashboard.monthExpenses')}
value={
summary?.current_month_by_currency?.expense && Object.keys(summary.current_month_by_currency.expense).length > 0
? Object.entries(summary.current_month_by_currency.expense).map(([curr, val]) => (
<div key={curr}>{currency(val, curr)}</div>
))
: currency(0, summary?.primary_currency || 'BRL')
}
valueColor="text-danger"
accentColor="#ef4444"
/>
</div>
<div className="col-6 col-lg-3">
<StatCard
icon="bi-hourglass-split"
label={t('dashboard.pending')}
value={summary?.pending?.count || 0}
subValue={summary?.overdue?.count > 0 ?
`⚠️ ${summary.overdue.count} ${t('dashboard.overdue')}` : null
}
accentColor="#f59e0b"
/>
</div>
</div>
{/* Calendário e Transações Futuras */}
<div className="row g-4 mb-4">
<div className="col-lg-8">
<CalendarWidget />
</div>
<div className="col-lg-4">
<UpcomingWidget />
</div>
</div>
{/* Transacciones Vencidas - Fila independiente */}
<div className="row g-4 mb-4">
<div className="col-12">
<OverdueWidget />
</div>
</div>
{/* Projeção de Saldo - Full Width */}
<div className="row g-4 mb-4">
<div className="col-12">
<BalanceProjectionChart />
</div>
</div>
{/* Main Content Grid */}
<div className="row g-4">
{/* Coluna Principal - Fluxo de Caixa */}
<div className="col-lg-8">
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
<div className="card-header border-0 d-flex justify-content-between align-items-center py-3"
style={{ background: 'transparent', borderBottom: '1px solid rgba(59, 130, 246, 0.2)' }}>
<h6 className="text-white mb-0 d-flex align-items-center">
<i className="bi bi-bar-chart-line me-2 text-primary"></i>
{t('dashboard.cashflow')}
</h6>
<div className="btn-group btn-group-sm" role="group">
{[6, 12, 24].map(months => (
<button
key={months}
type="button"
className={`btn ${cashflowMonths === months ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => handleMonthsChange(months)}
style={{ fontSize: '12px' }}
>
{months}m
</button>
))}
</div>
</div>
<div className="card-body">
<CashflowChart
data={cashflow?.data || []}
loading={cashflowLoading}
primaryCurrency={summary?.primary_currency || 'EUR'}
/>
{/* Resumo do período */}
{cashflow && !cashflowLoading && (
<div className="row mt-4 pt-3" style={{ borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<div className="col-6 col-md-3 text-center">
<small className="text-slate-500 d-block text-uppercase" style={{ fontSize: '10px' }}>
{t('dashboard.totalIncome')}
</small>
<span className="text-success fw-bold">
{currency(cashflow.totals?.income || 0, summary?.primary_currency || 'EUR')}
</span>
</div>
<div className="col-6 col-md-3 text-center">
<small className="text-slate-500 d-block text-uppercase" style={{ fontSize: '10px' }}>
{t('dashboard.totalExpenses')}
</small>
<span className="text-danger fw-bold">
{currency(cashflow.totals?.expense || 0, summary?.primary_currency || 'EUR')}
</span>
</div>
<div className="col-6 col-md-3 text-center mt-2 mt-md-0">
<small className="text-slate-500 d-block text-uppercase" style={{ fontSize: '10px' }}>
{t('dashboard.avgIncome')}
</small>
<span className="text-success">
{currency(cashflow.totals?.average_income || 0, summary?.primary_currency || 'EUR')}
</span>
</div>
<div className="col-6 col-md-3 text-center mt-2 mt-md-0">
<small className="text-slate-500 d-block text-uppercase" style={{ fontSize: '10px' }}>
{t('dashboard.avgExpense')}
</small>
<span className="text-danger">
{currency(cashflow.totals?.average_expense || 0, summary?.primary_currency || 'EUR')}
</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Coluna Lateral - Contas */}
<div className="col-lg-4">
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
<div className="card-header border-0 d-flex justify-content-between align-items-center py-3"
style={{ background: 'transparent', borderBottom: '1px solid rgba(59, 130, 246, 0.2)' }}>
<h6 className="text-white mb-0 d-flex align-items-center">
<i className="bi bi-bank me-2 text-primary"></i>
{t('dashboard.accountBalances')}
</h6>
<Link to="/accounts" className="btn btn-link btn-sm text-primary p-0" style={{ fontSize: '12px' }}>
{t('common.viewAll')} <i className="bi bi-arrow-right"></i>
</Link>
</div>
<div className="card-body py-2">
{loading ? (
<div className="placeholder-glow">
{[1, 2, 3].map(i => (
<div key={i} className="placeholder col-12 mb-2" style={{ height: '50px' }}></div>
))}
</div>
) : accounts.length === 0 ? (
<div className="text-center text-slate-400 py-4">
<i className="bi bi-bank fs-1 mb-2 d-block"></i>
<p className="small mb-2">{t('dashboard.noAccounts')}</p>
<Link to="/accounts" className="btn btn-primary btn-sm">
{t('dashboard.createAccount')}
</Link>
</div>
) : (
<div style={{ maxHeight: '320px', overflowY: 'auto' }}>
{accounts
.filter(a => a.is_active)
.sort((a, b) => Math.abs(b.current_balance) - Math.abs(a.current_balance))
.map(account => (
<AccountItem key={account.id} account={account} />
))}
</div>
)}
</div>
</div>
</div>
</div>
{/* Análise de Sobrepagamentos - Full Width */}
<div className="row mt-4">
<div className="col-12">
<OverpaymentsAnalysis
data={variances}
loading={variancesLoading}
onTransactionClick={handleTransactionClick}
/>
</div>
</div>
{/* CSS inline para efeitos hover */}
<style>{`
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.dashboard-container {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div>
);
};
export default Dashboard;