webmoney/frontend/src/pages/FinancialHealth.jsx
marcoitaloesp-ai 5f3bf18b99
v1.43.4 - Padronização de badges e botões em todo o sistema
- Badges: Estilo translúcido uniforme (bg-opacity-25 + text-color) via CSS global
- Afetados: RecurringTransactions, Accounts, Categories, TransactionsByWeek
- Widgets: UpcomingWidget, OverdueWidget, CalendarWidget, OverpaymentsAnalysis
- Botões: Estilo outline padronizado (btn-outline-*) em RecurringTransactions
- Simplificação: Remover classes redundantes dos JSX
2025-12-16 12:48:08 +00:00

1129 lines
50 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { financialHealthService } from '../services/api';
import useFormatters from '../hooks/useFormatters';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Title,
Tooltip,
Legend,
Filler,
} from 'chart.js';
import { Line, Doughnut, Bar } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Title,
Tooltip,
Legend,
Filler
);
const FinancialHealth = () => {
const { t } = useTranslation();
const { currency: formatCurrency, percent, number } = useFormatters();
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [history, setHistory] = useState([]);
const [activeTab, setActiveTab] = useState('overview');
// Helper para formatear moneda usando la moneda del API
const currency = (value, currencyCode = null) => {
const code = currencyCode || data?.currency || 'EUR';
return formatCurrency(value, code);
};
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const [healthData, historyData] = await Promise.all([
financialHealthService.get(),
financialHealthService.getHistory({ months: 6 })
]);
setData(healthData);
// Asegurar que history sea siempre un array
setHistory(Array.isArray(historyData) ? historyData : []);
} catch (error) {
console.error('Error loading financial health:', error);
} finally {
setLoading(false);
}
};
const getScoreColor = (score) => {
if (score >= 80) return '#10b981';
if (score >= 60) return '#84cc16';
if (score >= 40) return '#f59e0b';
if (score >= 20) return '#f97316';
return '#ef4444';
};
const getStatusColor = (status) => {
const colors = {
excellent: '#10b981',
good: '#84cc16',
adequate: '#22c55e',
moderate: '#f59e0b',
needs_improvement: '#f97316',
needs_attention: '#f97316',
needs_work: '#f97316',
negative: '#ef4444',
critical: '#ef4444',
insufficient: '#ef4444',
debt_free: '#10b981',
healthy: '#22c55e',
manageable: '#f59e0b',
concerning: '#ef4444',
on_track: '#10b981',
exceeded: '#ef4444',
not_configured: '#6b7280',
very_stable: '#10b981',
stable: '#22c55e',
volatile: '#ef4444',
optimized: '#10b981',
acceptable: '#f59e0b',
high_discretionary: '#f97316',
};
return colors[status] || '#6b7280';
};
const metricConfigs = {
savings_capacity: {
icon: 'bi-piggy-bank-fill',
gradient: ['#059669', '#10b981'],
},
debt_control: {
icon: 'bi-credit-card-2-front-fill',
gradient: ['#2563eb', '#3b82f6'],
},
budget_management: {
icon: 'bi-wallet2',
gradient: ['#7c3aed', '#8b5cf6'],
},
expense_efficiency: {
icon: 'bi-pie-chart-fill',
gradient: ['#0891b2', '#06b6d4'],
},
emergency_fund: {
icon: 'bi-shield-fill-check',
gradient: ['#d97706', '#f59e0b'],
},
financial_stability: {
icon: 'bi-graph-up-arrow',
gradient: ['#db2777', '#ec4899'],
},
};
const tabs = [
{ id: 'overview', icon: 'bi-speedometer2', label: t('financialHealth.tabs.overview') },
{ id: 'metrics', icon: 'bi-bar-chart-fill', label: t('financialHealth.tabs.metrics') },
{ id: 'categories', icon: 'bi-pie-chart-fill', label: t('financialHealth.tabs.categories') },
{ id: 'trends', icon: 'bi-graph-up', label: t('financialHealth.tabs.trends') },
{ id: 'insights', icon: 'bi-lightbulb-fill', label: t('financialHealth.tabs.insights') },
];
if (loading) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '400px' }}>
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">{t('common.loading')}</span>
</div>
</div>
);
}
if (!data) {
return (
<div className="alert alert-warning">
<i className="bi bi-exclamation-triangle me-2"></i>
{t('financialHealth.errorLoading')}
</div>
);
}
const score = data.overall_score;
const scoreColor = getScoreColor(score);
// Asegurar arrays válidos para Chart.js
const safeHistory = Array.isArray(history) ? history : [];
const safeMonthlyData = Array.isArray(data.trends?.monthly_data) ? data.trends.monthly_data : [];
const safeTopExpenses = Array.isArray(data.category_analysis?.top_expenses) ? data.category_analysis.top_expenses : [];
const safeCategoryTrends = Array.isArray(data.category_analysis?.category_trends) ? data.category_analysis.category_trends : [];
// Chart data for history
const historyChartData = safeHistory.length > 0 ? {
labels: safeHistory.map(h => h.month_label),
datasets: [{
label: t('financialHealth.score'),
data: safeHistory.map(h => h.score),
borderColor: scoreColor,
backgroundColor: `${scoreColor}20`,
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: scoreColor,
}],
} : null;
// Chart data for expense distribution
const distributionChartData = data.category_analysis?.expense_distribution ? {
labels: [
t('financialHealth.distribution.fixed'),
t('financialHealth.distribution.variable'),
t('financialHealth.distribution.discretionary'),
],
datasets: [{
data: [
data.category_analysis.expense_distribution.fixed.percentage,
data.category_analysis.expense_distribution.variable.percentage,
data.category_analysis.expense_distribution.discretionary.percentage,
],
backgroundColor: ['#3b82f6', '#22c55e', '#f59e0b'],
borderWidth: 0,
}],
} : null;
// Chart data for monthly comparison
const monthlyChartData = safeMonthlyData.length > 0 ? {
labels: safeMonthlyData.map(m => m.month),
datasets: [
{
label: t('financialHealth.income'),
data: safeMonthlyData.map(m => m.income),
backgroundColor: '#22c55e',
borderRadius: 4,
},
{
label: t('financialHealth.expenses'),
data: safeMonthlyData.map(m => m.expenses),
backgroundColor: '#ef4444',
borderRadius: 4,
},
],
} : null;
// Category expenses chart
const categoryChartData = safeTopExpenses.length > 0 ? {
labels: safeTopExpenses.slice(0, 8).map(c => c.name),
datasets: [{
data: safeTopExpenses.slice(0, 8).map(c => c.total),
backgroundColor: safeTopExpenses.slice(0, 8).map(c => c.color || '#6b7280'),
borderWidth: 0,
}],
} : null;
return (
<div className="financial-health-container">
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 className="text-white mb-1 fw-bold">
<i className="bi bi-heart-pulse me-2"></i>
{t('financialHealth.title')}
</h4>
<p className="text-slate-400 mb-0 small">
{t('financialHealth.subtitle')} {t('financialHealth.lastUpdate')}: {new Date(data.last_updated).toLocaleDateString()}
</p>
</div>
<div className="d-flex gap-2">
<span className="badge bg-primary">{data.currency}</span>
<button className="btn btn-outline-primary btn-sm" onClick={loadData}>
<i className="bi bi-arrow-clockwise me-1"></i>
{t('common.refresh')}
</button>
</div>
</div>
{/* Tabs */}
<ul className="nav nav-pills mb-4 gap-2">
{tabs.map(tab => (
<li key={tab.id} className="nav-item">
<button
className={`nav-link ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
style={{
background: activeTab === tab.id ? '#3b82f6' : '#1e293b',
color: activeTab === tab.id ? 'white' : '#94a3b8',
border: 'none',
}}
>
<i className={`bi ${tab.icon} me-2`}></i>
{tab.label}
</button>
</li>
))}
</ul>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="row g-4">
{/* Score Circle */}
<div className="col-lg-4">
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
<div className="card-body text-center py-4">
{/* Score Ring */}
<div className="position-relative d-inline-block mb-3">
<svg width="180" height="180" viewBox="0 0 180 180">
<circle cx="90" cy="90" r="75" fill="none" stroke="#1e293b" strokeWidth="12" />
<circle
cx="90" cy="90" r="75" fill="none"
stroke={scoreColor}
strokeWidth="12"
strokeLinecap="round"
strokeDasharray={`${(score / 100) * 471} 471`}
transform="rotate(-90 90 90)"
style={{ filter: `drop-shadow(0 0 8px ${scoreColor}50)`, transition: 'stroke-dasharray 1s ease-in-out' }}
/>
<text x="90" y="80" textAnchor="middle" fill={scoreColor} fontSize="42" fontWeight="bold">{score}</text>
<text x="90" y="105" textAnchor="middle" fill="#94a3b8" fontSize="14">{t('financialHealth.outOf100')}</text>
</svg>
</div>
<h5 className="mb-2" style={{ color: data.health_level?.color }}>
{t(`financialHealth.levels.${data.health_level?.level}`)}
</h5>
<p className="text-slate-400 small mb-3">{t('financialHealth.scoreDescription')}</p>
{/* Mini History Chart */}
{historyChartData && (
<div style={{ height: '80px' }}>
<Line
data={historyChartData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: { display: false, min: 0, max: 100 },
},
}}
/>
</div>
)}
</div>
</div>
</div>
{/* Summary Cards */}
<div className="col-lg-8">
<div className="row g-3">
{/* Net Worth */}
<div className="col-md-6">
<div className="card border-0 h-100" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-start">
<div>
<small className="text-slate-400">{t('financialHealth.summary.netWorth')}</small>
<h4 className={`mb-0 fw-bold ${data.summary?.net_worth >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(data.summary?.net_worth || 0)}
</h4>
</div>
<i className="bi bi-bank fs-3 text-primary opacity-50"></i>
</div>
<div className="mt-2 d-flex gap-3 small">
<span className="text-success">
<i className="bi bi-arrow-up me-1"></i>
{t('financialHealth.summary.assets')}: {currency(data.summary?.total_assets || 0)}
</span>
<span className="text-danger">
<i className="bi bi-arrow-down me-1"></i>
{t('financialHealth.summary.liabilities')}: {currency(data.summary?.total_liabilities || 0)}
</span>
</div>
</div>
</div>
</div>
{/* Monthly Savings */}
<div className="col-md-6">
<div className="card border-0 h-100" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-start">
<div>
<small className="text-slate-400">{t('financialHealth.summary.monthlySavings')}</small>
<h4 className={`mb-0 fw-bold ${data.summary?.monthly_savings >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(data.summary?.monthly_savings || 0)}
</h4>
</div>
<i className="bi bi-piggy-bank fs-3 text-success opacity-50"></i>
</div>
<div className="mt-2 small">
<span className="text-slate-400">
{t('financialHealth.summary.savingsRate')}:
<span className={data.summary?.savings_rate >= 20 ? 'text-success' : 'text-warning'}>
{' '}{data.summary?.savings_rate || 0}%
</span>
</span>
</div>
</div>
</div>
</div>
{/* Monthly Income */}
<div className="col-md-4">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body py-3">
<small className="text-slate-400 d-block">{t('financialHealth.summary.monthlyIncome')}</small>
<h5 className="text-success mb-0">{currency(data.summary?.monthly_income || 0)}</h5>
{data.trends?.monthly_comparison?.income?.change !== 0 && (
<small className={data.trends.monthly_comparison.income.change > 0 ? 'text-success' : 'text-danger'}>
<i className={`bi bi-arrow-${data.trends.monthly_comparison.income.change > 0 ? 'up' : 'down'} me-1`}></i>
{Math.abs(data.trends.monthly_comparison.income.change)}% {t('financialHealth.vsLastMonth')}
</small>
)}
</div>
</div>
</div>
{/* Monthly Expenses */}
<div className="col-md-4">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body py-3">
<small className="text-slate-400 d-block">{t('financialHealth.summary.monthlyExpenses')}</small>
<h5 className="text-danger mb-0">{currency(data.summary?.monthly_expenses || 0)}</h5>
{data.trends?.monthly_comparison?.expenses?.change !== 0 && (
<small className={data.trends.monthly_comparison.expenses.change < 0 ? 'text-success' : 'text-danger'}>
<i className={`bi bi-arrow-${data.trends.monthly_comparison.expenses.change > 0 ? 'up' : 'down'} me-1`}></i>
{Math.abs(data.trends.monthly_comparison.expenses.change)}% {t('financialHealth.vsLastMonth')}
</small>
)}
</div>
</div>
</div>
{/* Projection */}
<div className="col-md-4">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body py-3">
<small className="text-slate-400 d-block">{t('financialHealth.summary.projectedSavings')}</small>
<h5 className={`mb-0 ${data.projection?.projected?.savings >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(data.projection?.projected?.savings || 0)}
</h5>
<small className="text-slate-500">
{data.projection?.days_remaining} {t('financialHealth.daysRemaining')}
</small>
</div>
</div>
</div>
{/* Accounts by Currency */}
{data.summary?.accounts_by_currency?.length > 1 && (
<div className="col-12">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body py-3">
<small className="text-slate-400 d-block mb-2">
<i className="bi bi-currency-exchange me-1"></i>
{t('financialHealth.summary.byCurrency')}
</small>
<div className="d-flex flex-wrap gap-3">
{data.summary.accounts_by_currency.map((curr, idx) => (
<div key={idx} className="d-flex align-items-center">
<span className="badge bg-primary me-2">{curr.currency}</span>
<span className={curr.balance >= 0 ? 'text-success' : 'text-danger'}>
{currency(curr.balance, curr.currency)}
</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Metrics Tab */}
{activeTab === 'metrics' && (
<div className="row g-3">
{Object.entries(data.metrics || {}).map(([key, metric]) => {
const config = metricConfigs[key];
if (!config) return null;
return (
<div key={key} className="col-md-6 col-lg-4">
<div
className="card border-0 h-100"
style={{
background: `linear-gradient(135deg, ${config.gradient[0]} 0%, ${config.gradient[1]} 100%)`,
}}
>
<div className="card-body text-white">
<div className="d-flex align-items-center justify-content-between mb-3">
<div className="d-flex align-items-center">
<i className={`bi ${config.icon} fs-4 me-2 opacity-75`}></i>
<span className="fw-medium">{t(`financialHealth.metrics.${key}`)}</span>
</div>
<span
className="badge"
style={{
background: 'rgba(255,255,255,0.2)',
color: 'white',
}}
>
{t(`financialHealth.status.${metric.status}`)}
</span>
</div>
<div className="d-flex align-items-end mb-3">
<h2 className="mb-0 fw-bold">{metric.score}</h2>
<span className="ms-2 opacity-75">/100</span>
</div>
{/* Progress bar */}
<div className="progress bg-white bg-opacity-25 mb-3" style={{ height: '6px' }}>
<div className="progress-bar bg-white" style={{ width: `${metric.score}%` }}></div>
</div>
{/* Metric-specific details */}
<div className="small opacity-90">
{key === 'savings_capacity' && (
<>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.savingsRate')}:</span>
<span>{metric.savings_rate}%</span>
</div>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.monthlySavings')}:</span>
<span>{currency(metric.monthly_savings)}</span>
</div>
</>
)}
{key === 'debt_control' && (
<>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.totalDebt')}:</span>
<span>{currency(metric.total_debt)}</span>
</div>
{/* Multi-currency breakdown */}
{metric.debt_by_currency && Object.keys(metric.debt_by_currency).length > 1 && (
<div className="d-flex gap-1 flex-wrap mb-2">
{Object.entries(metric.debt_by_currency).map(([curr, amt]) => (
<span key={curr} className="badge bg-danger" style={{ fontSize: '0.65rem' }}>
{curr}: {Number(amt).toLocaleString(undefined, {minimumFractionDigits: 2})}
</span>
))}
</div>
)}
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.debtToIncome')}:</span>
<span>{metric.debt_to_income_ratio}%</span>
</div>
{metric.active_debts > 0 && (
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.activeDebts')}:</span>
<span>{metric.active_debts}</span>
</div>
)}
</>
)}
{key === 'budget_management' && (
<>
{metric.has_budgets ? (
<>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.budgetsConfigured')}:</span>
<span>{metric.total_budgets}</span>
</div>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.compliance')}:</span>
<span>{metric.compliance_rate}%</span>
</div>
{metric.exceeded_count > 0 && (
<div className="d-flex justify-content-between text-warning">
<span>{t('financialHealth.details.exceeded')}:</span>
<span>{metric.exceeded_count}</span>
</div>
)}
</>
) : (
<span className="opacity-75">{t('financialHealth.details.noBudgets')}</span>
)}
</>
)}
{key === 'expense_efficiency' && (
<>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.distribution.fixed')}:</span>
<span>{metric.distribution?.fixed?.percentage}%</span>
</div>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.distribution.variable')}:</span>
<span>{metric.distribution?.variable?.percentage}%</span>
</div>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.distribution.discretionary')}:</span>
<span>{metric.distribution?.discretionary?.percentage}%</span>
</div>
</>
)}
{key === 'emergency_fund' && (
<>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.liquidAssets')}:</span>
<span>{currency(metric.liquid_assets)}</span>
</div>
{/* Multi-currency breakdown */}
{metric.liquid_assets_by_currency && Object.keys(metric.liquid_assets_by_currency).length > 1 && (
<div className="d-flex gap-1 flex-wrap mb-2">
{Object.entries(metric.liquid_assets_by_currency).map(([curr, amt]) => (
<span key={curr} className="badge bg-primary" style={{ fontSize: '0.65rem' }}>
{curr}: {Number(amt).toLocaleString(undefined, {minimumFractionDigits: 2})}
</span>
))}
</div>
)}
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.monthsCovered')}:</span>
<span>{metric.months_covered} {t('common.months')}</span>
</div>
{metric.gap > 0 && (
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.gap')}:</span>
<span>{currency(metric.gap)}</span>
</div>
)}
</>
)}
{key === 'financial_stability' && (
<>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.incomeVolatility')}:</span>
<span>{metric.income_volatility}%</span>
</div>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.expenseVolatility')}:</span>
<span>{metric.expense_volatility}%</span>
</div>
<div className="d-flex justify-content-between">
<span>{t('financialHealth.details.savingsTrend')}:</span>
<span>
<i className={`bi bi-arrow-${metric.savings_trend === 'increasing' ? 'up text-success' : metric.savings_trend === 'decreasing' ? 'down text-danger' : 'right text-warning'}`}></i>
{' '}{t(`financialHealth.trend.${metric.savings_trend}`)}
</span>
</div>
</>
)}
</div>
</div>
</div>
</div>
);
})}
</div>
)}
{/* Categories Tab */}
{activeTab === 'categories' && (
<div className="row g-4">
{/* Expense Distribution */}
<div className="col-lg-4">
<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-pie-chart me-2"></i>
{t('financialHealth.categories.distribution')}
</h6>
</div>
<div className="card-body">
{distributionChartData && (
<div style={{ height: '200px' }}>
<Doughnut
data={distributionChartData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#94a3b8' },
},
},
cutout: '60%',
}}
/>
</div>
)}
<div className="mt-3">
{data.category_analysis?.expense_distribution && Object.entries(data.category_analysis.expense_distribution).map(([key, val]) => (
<div key={key} className="mb-2">
<div className="d-flex justify-content-between small text-slate-400">
<span>{t(`financialHealth.distribution.${key}`)}</span>
<span>{currency(val.amount)} ({val.percentage}%)</span>
</div>
{/* Multi-currency breakdown */}
{val.by_currency && Object.keys(val.by_currency).length > 1 && (
<div className="d-flex gap-1 mt-1 flex-wrap">
{Object.entries(val.by_currency).map(([curr, amt]) => (
<span key={curr} className="badge bg-secondary" style={{ fontSize: '0.6rem' }}>
{curr}: {Number(amt).toLocaleString()}
</span>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
{/* Top Expenses */}
<div className="col-lg-8">
<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 me-2"></i>
{t('financialHealth.categories.topExpenses')}
</h6>
</div>
<div className="card-body">
<div className="row g-2">
{data.category_analysis?.top_expenses?.slice(0, 10).map((cat, idx) => (
<div key={idx} className="col-12">
<div className="d-flex align-items-center mb-2">
<div
className="rounded-circle me-2 d-flex align-items-center justify-content-center"
style={{
width: '32px',
height: '32px',
background: cat.color || '#6b7280',
minWidth: '32px',
}}
>
<i className={`bi ${cat.icon || 'bi-tag'} text-white small`}></i>
</div>
<div className="flex-grow-1">
<div className="d-flex justify-content-between align-items-center">
<span className="text-white small">{cat.name}</span>
<div className="text-end">
<span className="text-slate-400 small">
{currency(cat.total)} ({cat.percentage}%)
</span>
{/* Multi-currency breakdown */}
{cat.by_currency && Object.keys(cat.by_currency).length > 1 && (
<div className="d-flex gap-2 justify-content-end mt-1">
{Object.entries(cat.by_currency).map(([curr, amt]) => (
<span key={curr} className="badge bg-secondary small" style={{ fontSize: '0.65rem' }}>
{curr}: {Number(amt).toLocaleString(undefined, { minimumFractionDigits: 2 })}
</span>
))}
</div>
)}
</div>
</div>
<div className="progress mt-1" style={{ height: '4px', background: '#1e293b' }}>
<div
className="progress-bar"
style={{
width: `${cat.percentage}%`,
background: cat.color || '#6b7280',
}}
></div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Category Trends */}
{data.category_analysis?.category_trends?.length > 0 && (
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-arrow-left-right me-2"></i>
{t('financialHealth.categories.trends')}
</h6>
</div>
<div className="card-body">
<div className="row g-3">
{safeCategoryTrends.map((trend, idx) => (
<div key={idx} className="col-md-4 col-lg-2">
<div
className="p-3 rounded text-center"
style={{
background: '#1e293b',
borderLeft: `3px solid ${trend.trend === 'increasing' ? '#ef4444' : '#22c55e'}`,
}}
>
<small className="text-slate-400 d-block">{trend.category}</small>
<div className={`fs-5 fw-bold ${trend.trend === 'increasing' ? 'text-danger' : 'text-success'}`}>
<i className={`bi bi-arrow-${trend.trend === 'increasing' ? 'up' : 'down'} me-1`}></i>
{Math.abs(trend.change_percent)}%
</div>
<small className="text-slate-500">
{currency(trend.previous)} {currency(trend.current)}
</small>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* Trends Tab */}
{activeTab === 'trends' && (
<div className="row g-4">
{/* Monthly Evolution Chart */}
<div className="col-lg-8">
<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-graph-up me-2"></i>
{t('financialHealth.trends.monthlyEvolution')}
</h6>
</div>
<div className="card-body">
{monthlyChartData && (
<div style={{ height: '300px' }}>
<Bar
data={monthlyChartData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: { color: '#94a3b8' },
},
},
scales: {
x: {
grid: { color: '#1e293b' },
ticks: { color: '#94a3b8' },
},
y: {
grid: { color: '#1e293b' },
ticks: { color: '#94a3b8' },
},
},
}}
/>
</div>
)}
</div>
</div>
</div>
{/* Trend Indicators */}
<div className="col-lg-4">
<div className="row g-3">
{/* Income Trend */}
<div className="col-12">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-slate-400">{t('financialHealth.trends.incomeTrend')}</small>
<div className="d-flex align-items-center">
<i className={`bi bi-arrow-${data.trends?.income_trend?.direction === 'increasing' ? 'up text-success' : data.trends?.income_trend?.direction === 'decreasing' ? 'down text-danger' : 'right text-warning'} fs-4 me-2`}></i>
<span className="text-white">{t(`financialHealth.trend.${data.trends?.income_trend?.direction}`)}</span>
</div>
</div>
<span className="badge bg-success">
{data.trends?.income_trend?.strength}%
</span>
</div>
</div>
</div>
</div>
{/* Expense Trend */}
<div className="col-12">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-slate-400">{t('financialHealth.trends.expenseTrend')}</small>
<div className="d-flex align-items-center">
<i className={`bi bi-arrow-${data.trends?.expense_trend?.direction === 'increasing' ? 'up text-danger' : data.trends?.expense_trend?.direction === 'decreasing' ? 'down text-success' : 'right text-warning'} fs-4 me-2`}></i>
<span className="text-white">{t(`financialHealth.trend.${data.trends?.expense_trend?.direction}`)}</span>
</div>
</div>
<span className={`badge ${data.trends?.expense_trend?.direction === 'decreasing' ? 'bg-success' : 'bg-danger'} bg-opacity-25 ${data.trends?.expense_trend?.direction === 'decreasing' ? 'text-success' : 'text-danger'}`}>
{data.trends?.expense_trend?.strength}%
</span>
</div>
</div>
</div>
</div>
{/* Savings Trend */}
<div className="col-12">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-slate-400">{t('financialHealth.trends.savingsTrend')}</small>
<div className="d-flex align-items-center">
<i className={`bi bi-arrow-${data.trends?.savings_trend?.direction === 'increasing' ? 'up text-success' : data.trends?.savings_trend?.direction === 'decreasing' ? 'down text-danger' : 'right text-warning'} fs-4 me-2`}></i>
<span className="text-white">{t(`financialHealth.trend.${data.trends?.savings_trend?.direction}`)}</span>
</div>
</div>
<span className={`badge ${data.trends?.savings_trend?.direction === 'increasing' ? 'bg-success' : 'bg-danger'} bg-opacity-25 ${data.trends?.savings_trend?.direction === 'increasing' ? 'text-success' : 'text-danger'}`}>
{data.trends?.savings_trend?.strength}%
</span>
</div>
</div>
</div>
</div>
{/* Monthly Comparison */}
<div className="col-12">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400 d-block mb-2">{t('financialHealth.trends.monthlyComparison')}</small>
<div className="row g-2 text-center">
<div className="col-6">
<small className="text-slate-500 d-block">{t('financialHealth.income')}</small>
<span className={data.trends?.monthly_comparison?.income?.change > 0 ? 'text-success' : 'text-danger'}>
{data.trends?.monthly_comparison?.income?.change > 0 ? '+' : ''}{data.trends?.monthly_comparison?.income?.change}%
</span>
</div>
<div className="col-6">
<small className="text-slate-500 d-block">{t('financialHealth.expenses')}</small>
<span className={data.trends?.monthly_comparison?.expenses?.change < 0 ? 'text-success' : 'text-danger'}>
{data.trends?.monthly_comparison?.expenses?.change > 0 ? '+' : ''}{data.trends?.monthly_comparison?.expenses?.change}%
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Score History */}
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-clock-history me-2"></i>
{t('financialHealth.trends.scoreHistory')}
</h6>
</div>
<div className="card-body">
{safeHistory.length > 0 && (
<div style={{ height: '200px' }}>
<Line
data={{
labels: safeHistory.map(h => h.month_label),
datasets: [
{
label: t('financialHealth.score'),
data: safeHistory.map(h => h.score),
borderColor: '#3b82f6',
backgroundColor: '#3b82f620',
fill: true,
tension: 0.4,
},
{
label: t('financialHealth.savingsRate'),
data: safeHistory.map(h => h.savings_rate),
borderColor: '#22c55e',
backgroundColor: 'transparent',
borderDash: [5, 5],
tension: 0.4,
},
],
}}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: { color: '#94a3b8' },
},
},
scales: {
x: {
grid: { color: '#1e293b' },
ticks: { color: '#94a3b8' },
},
y: {
grid: { color: '#1e293b' },
ticks: { color: '#94a3b8' },
min: 0,
max: 100,
},
},
}}
/>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Insights Tab */}
{activeTab === 'insights' && (
<div className="row g-4">
{/* Insights */}
<div className="col-lg-8">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-lightbulb me-2"></i>
{t('financialHealth.insightsTitle')}
</h6>
</div>
<div className="card-body">
{data.insights?.length > 0 ? (
<div className="d-flex flex-column gap-3">
{data.insights.map((insight, idx) => (
<div
key={idx}
className="d-flex p-3 rounded"
style={{
background: '#1e293b',
borderLeft: `4px solid ${insight.color}`,
}}
>
<i
className={`bi ${insight.icon} fs-4 me-3`}
style={{ color: insight.color }}
></i>
<div>
<h6 className="text-white mb-1">
{t(insight.title_key, insight.data)}
</h6>
<p className="text-slate-400 mb-0 small">
{t(insight.message_key, insight.data)}
</p>
</div>
</div>
))}
</div>
) : (
<p className="text-slate-400 text-center py-4">
{t('financialHealth.noInsights')}
</p>
)}
</div>
</div>
</div>
{/* Recommendations */}
<div className="col-lg-4">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-magic me-2"></i>
{t('financialHealth.recommendationsTitle')}
</h6>
</div>
<div className="card-body">
{data.recommendations?.length > 0 ? (
<div className="d-flex flex-column gap-2">
{data.recommendations.map((rec, idx) => (
<div
key={idx}
className="p-3 rounded"
style={{ background: '#1e293b' }}
>
<div className="d-flex align-items-start">
<span
className={`badge me-2 ${rec.priority === 'high' ? 'bg-danger' : 'bg-warning'}`}
>
{rec.priority === 'high' ? t('financialHealth.priority.high') : t('financialHealth.priority.medium')}
</span>
</div>
<p className="text-white small mb-2 mt-2">
{t(rec.action_key, rec)}
</p>
{rec.target_amount && (
<small className="text-slate-400">
{t('financialHealth.target')}: {currency(rec.target_amount)}
</small>
)}
{rec.monthly_suggestion && (
<small className="text-slate-400 d-block">
{t('financialHealth.monthlyTarget')}: {currency(rec.monthly_suggestion)}
</small>
)}
</div>
))}
</div>
) : (
<p className="text-slate-400 text-center py-4">
<i className="bi bi-check-circle fs-1 d-block mb-2 text-success"></i>
{t('financialHealth.noRecommendations')}
</p>
)}
</div>
</div>
{/* Projection */}
{data.projection && (
<div className="card border-0 mt-3" style={{ background: '#1e293b' }}>
<div className="card-body">
<h6 className="text-white mb-3">
<i className="bi bi-calendar-check me-2"></i>
{t('financialHealth.projection.title')}
</h6>
<div className="row g-2 text-center">
<div className="col-6">
<small className="text-slate-500 d-block">{t('financialHealth.projection.currentExpenses')}</small>
<span className="text-danger fw-bold">
{currency(data.projection.current_month?.expenses || 0)}
</span>
</div>
<div className="col-6">
<small className="text-slate-500 d-block">{t('financialHealth.projection.projected')}</small>
<span className="text-warning fw-bold">
{currency(data.projection.projected?.expenses || 0)}
</span>
</div>
<div className="col-12 mt-2">
<small className="text-slate-500">
{data.projection.days_remaining} {t('financialHealth.daysRemaining')}
{data.projection.vs_average?.expenses !== 0 && (
<span className={data.projection.vs_average.expenses > 0 ? 'text-danger' : 'text-success'}>
{' '}({data.projection.vs_average.expenses > 0 ? '+' : ''}{data.projection.vs_average.expenses}% {t('financialHealth.vsAverage')})
</span>
)}
</small>
</div>
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};
export default FinancialHealth;