Fix: Garantir herança de cor em subcategorias
- Usar displayColor ao passar cor para filhos (não category.color) - Fallback para cor padrão se categoria não tiver cor - Garante que todas as subcategorias usem a mesma cor da categoria pai
This commit is contained in:
parent
1637e5da0c
commit
b0724d7b2c
@ -250,8 +250,8 @@ const Categories = () => {
|
|||||||
const renderCategory = (category, level = 0, parentColor = null) => {
|
const renderCategory = (category, level = 0, parentColor = null) => {
|
||||||
const hasChildren = category.children && category.children.length > 0;
|
const hasChildren = category.children && category.children.length > 0;
|
||||||
const isExpanded = expandedCategories[category.id];
|
const isExpanded = expandedCategories[category.id];
|
||||||
// Subcategorias herdam a cor da categoria pai
|
// Subcategorias herdam a cor da categoria pai - garantir que sempre use a cor do pai se disponível
|
||||||
const displayColor = level > 0 && parentColor ? parentColor : category.color;
|
const displayColor = level > 0 && parentColor ? parentColor : (category.color || '#94a3b8');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={category.id}>
|
<div key={category.id}>
|
||||||
@ -343,7 +343,7 @@ const Categories = () => {
|
|||||||
{/* Children */}
|
{/* Children */}
|
||||||
{hasChildren && isExpanded && (
|
{hasChildren && isExpanded && (
|
||||||
<div>
|
<div>
|
||||||
{category.children.map(child => renderCategory(child, level + 1, category.color))}
|
{category.children.map(child => renderCategory(child, level + 1, displayColor))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,6 +35,15 @@ const Reports = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currency, date: formatDate } = useFormatters();
|
const { currency, date: formatDate } = useFormatters();
|
||||||
|
|
||||||
|
// Mobile detection
|
||||||
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState('summary');
|
const [activeTab, setActiveTab] = useState('summary');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [year, setYear] = useState(new Date().getFullYear());
|
const [year, setYear] = useState(new Date().getFullYear());
|
||||||
@ -178,11 +187,11 @@ const Reports = () => {
|
|||||||
const monthlyAvgBalance = (summary.current.balance / 12).toFixed(2);
|
const monthlyAvgBalance = (summary.current.balance / 12).toFixed(2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row g-4">
|
<div className="row g-3">
|
||||||
{/* Year Selector */}
|
{/* Year Selector */}
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h5 className="text-white mb-0">
|
<h5 className="text-white mb-0" style={{ fontSize: isMobile ? '0.95rem' : '1.25rem' }}>
|
||||||
<i className="bi bi-calendar3 me-2"></i>
|
<i className="bi bi-calendar3 me-2"></i>
|
||||||
{t('reports.annualSummary')}
|
{t('reports.annualSummary')}
|
||||||
</h5>
|
</h5>
|
||||||
@ -190,8 +199,12 @@ const Reports = () => {
|
|||||||
{[2024, 2025].map(y => (
|
{[2024, 2025].map(y => (
|
||||||
<button
|
<button
|
||||||
key={y}
|
key={y}
|
||||||
className={`btn btn-sm ${year === y ? 'btn-primary' : 'btn-outline-secondary'}`}
|
className={`btn ${year === y ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||||
onClick={() => setYear(y)}
|
onClick={() => setYear(y)}
|
||||||
|
style={{
|
||||||
|
fontSize: isMobile ? '0.75rem' : '0.875rem',
|
||||||
|
padding: isMobile ? '0.25rem 0.75rem' : '0.375rem 1rem'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{y}
|
{y}
|
||||||
</button>
|
</button>
|
||||||
@ -201,29 +214,38 @@ const Reports = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main KPI Cards */}
|
{/* Main KPI Cards */}
|
||||||
<div className="col-lg-3 col-md-6">
|
<div className={isMobile ? "col-12" : "col-lg-3 col-md-6"}>
|
||||||
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #059669 0%, #047857 100%)' }}>
|
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #059669 0%, #047857 100%)' }}>
|
||||||
<div className="card-body text-white">
|
<div className="card-body text-white" style={{ padding: isMobile ? '0.75rem' : '1rem' }}>
|
||||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
<div className="d-flex align-items-center justify-content-between mb-2">
|
||||||
<div>
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.income')}</div>
|
<div className="opacity-75 text-uppercase fw-semibold mb-1" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
|
||||||
<h3 className="mb-0 fw-bold">{currency(summary.current.income, summary.currency)}</h3>
|
{t('reports.income')}
|
||||||
</div>
|
</div>
|
||||||
<div className="fs-1 opacity-50">
|
<h3 className="mb-0 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
|
||||||
<i className="bi bi-arrow-up-circle"></i>
|
{currency(summary.current.income, summary.currency)}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
{!isMobile && (
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-arrow-up-circle"></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{summary.variation.income !== 0 && !isNaN(summary.variation.income) && isFinite(summary.variation.income) && (
|
{summary.variation.income !== 0 && !isNaN(summary.variation.income) && isFinite(summary.variation.income) && (
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<span className={`badge ${summary.variation.income >= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
|
<span className={`badge ${summary.variation.income >= 0 ? 'bg-white text-success' : 'bg-white text-danger'}`}
|
||||||
|
style={{ fontSize: isMobile ? '0.65rem' : '0.75rem', padding: isMobile ? '0.25rem 0.5rem' : '0.375rem 0.75rem' }}>
|
||||||
<i className={`bi bi-arrow-${summary.variation.income >= 0 ? 'up' : 'down'} me-1`}></i>
|
<i className={`bi bi-arrow-${summary.variation.income >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||||
{Math.abs(summary.variation.income).toFixed(1)}%
|
{Math.abs(summary.variation.income).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<small className="opacity-75">{t('reports.vsLastYear')}</small>
|
<small className="opacity-75" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
|
||||||
|
{t('reports.vsLastYear')}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<hr className="my-2 opacity-25" />
|
<hr className="my-2 opacity-25" />
|
||||||
<div className="small d-flex justify-content-between">
|
<div className="d-flex justify-content-between" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
|
||||||
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
||||||
<span className="fw-semibold">{currency(monthlyAvgIncome, summary.currency)}</span>
|
<span className="fw-semibold">{currency(monthlyAvgIncome, summary.currency)}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -231,29 +253,38 @@ const Reports = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-lg-3 col-md-6">
|
<div className={isMobile ? "col-12" : "col-lg-3 col-md-6"}>
|
||||||
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)' }}>
|
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)' }}>
|
||||||
<div className="card-body text-white">
|
<div className="card-body text-white" style={{ padding: isMobile ? '0.75rem' : '1rem' }}>
|
||||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
<div className="d-flex align-items-center justify-content-between mb-2">
|
||||||
<div>
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.expenses')}</div>
|
<div className="opacity-75 text-uppercase fw-semibold mb-1" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
|
||||||
<h3 className="mb-0 fw-bold">{currency(summary.current.expense, summary.currency)}</h3>
|
{t('reports.expenses')}
|
||||||
</div>
|
</div>
|
||||||
<div className="fs-1 opacity-50">
|
<h3 className="mb-0 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
|
||||||
<i className="bi bi-arrow-down-circle"></i>
|
{currency(summary.current.expense, summary.currency)}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
{!isMobile && (
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-arrow-down-circle"></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{summary.variation.expense !== 0 && !isNaN(summary.variation.expense) && isFinite(summary.variation.expense) && (
|
{summary.variation.expense !== 0 && !isNaN(summary.variation.expense) && isFinite(summary.variation.expense) && (
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<span className={`badge ${summary.variation.expense <= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
|
<span className={`badge ${summary.variation.expense <= 0 ? 'bg-white text-success' : 'bg-white text-danger'}`}
|
||||||
|
style={{ fontSize: isMobile ? '0.65rem' : '0.75rem', padding: isMobile ? '0.25rem 0.5rem' : '0.375rem 0.75rem' }}>
|
||||||
<i className={`bi bi-arrow-${summary.variation.expense >= 0 ? 'up' : 'down'} me-1`}></i>
|
<i className={`bi bi-arrow-${summary.variation.expense >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||||
{Math.abs(summary.variation.expense).toFixed(1)}%
|
{Math.abs(summary.variation.expense).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<small className="opacity-75">{t('reports.vsLastYear')}</small>
|
<small className="opacity-75" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
|
||||||
|
{t('reports.vsLastYear')}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<hr className="my-2 opacity-25" />
|
<hr className="my-2 opacity-25" />
|
||||||
<div className="small d-flex justify-content-between">
|
<div className="d-flex justify-content-between" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
|
||||||
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
||||||
<span className="fw-semibold">{currency(monthlyAvgExpense, summary.currency)}</span>
|
<span className="fw-semibold">{currency(monthlyAvgExpense, summary.currency)}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -261,29 +292,38 @@ const Reports = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-lg-3 col-md-6">
|
<div className={isMobile ? "col-12" : "col-lg-3 col-md-6"}>
|
||||||
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' }}>
|
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' }}>
|
||||||
<div className="card-body text-white">
|
<div className="card-body text-white" style={{ padding: isMobile ? '0.75rem' : '1rem' }}>
|
||||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
<div className="d-flex align-items-center justify-content-between mb-2">
|
||||||
<div>
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.balance')}</div>
|
<div className="opacity-75 text-uppercase fw-semibold mb-1" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
|
||||||
<h3 className="mb-0 fw-bold">{currency(summary.current.balance, summary.currency)}</h3>
|
{t('reports.balance')}
|
||||||
</div>
|
</div>
|
||||||
<div className="fs-1 opacity-50">
|
<h3 className="mb-0 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
|
||||||
<i className="bi bi-wallet2"></i>
|
{currency(summary.current.balance, summary.currency)}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
{!isMobile && (
|
||||||
|
<div className="fs-1 opacity-50">
|
||||||
|
<i className="bi bi-wallet2"></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{summary.variation.balance !== 0 && !isNaN(summary.variation.balance) && isFinite(summary.variation.balance) && (
|
{summary.variation.balance !== 0 && !isNaN(summary.variation.balance) && isFinite(summary.variation.balance) && (
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<span className={`badge ${summary.variation.balance >= 0 ? 'bg-white text-success' : 'bg-white text-danger'} px-2 py-1`}>
|
<span className={`badge ${summary.variation.balance >= 0 ? 'bg-white text-success' : 'bg-white text-danger'}`}
|
||||||
|
style={{ fontSize: isMobile ? '0.65rem' : '0.75rem', padding: isMobile ? '0.25rem 0.5rem' : '0.375rem 0.75rem' }}>
|
||||||
<i className={`bi bi-arrow-${summary.variation.balance >= 0 ? 'up' : 'down'} me-1`}></i>
|
<i className={`bi bi-arrow-${summary.variation.balance >= 0 ? 'up' : 'down'} me-1`}></i>
|
||||||
{Math.abs(summary.variation.balance).toFixed(1)}%
|
{Math.abs(summary.variation.balance).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<small className="opacity-75">{t('reports.vsLastYear')}</small>
|
<small className="opacity-75" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
|
||||||
|
{t('reports.vsLastYear')}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<hr className="my-2 opacity-25" />
|
<hr className="my-2 opacity-25" />
|
||||||
<div className="small d-flex justify-content-between">
|
<div className="d-flex justify-content-between" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
|
||||||
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
<span className="opacity-75">{t('reports.monthlyAverage')}:</span>
|
||||||
<span className="fw-semibold">{currency(monthlyAvgBalance, summary.currency)}</span>
|
<span className="fw-semibold">{currency(monthlyAvgBalance, summary.currency)}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1761,28 +1801,34 @@ const Reports = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="reports-container">
|
<div className="reports-container">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
<div className={`d-flex ${isMobile ? 'flex-column gap-2' : 'justify-content-between align-items-center'} mb-4`}>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-white mb-1 fw-bold">
|
<h4 className="text-white mb-1 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
|
||||||
<i className="bi bi-bar-chart-line me-2"></i>
|
<i className="bi bi-bar-chart-line me-2"></i>
|
||||||
{t('reports.title')}
|
{t('reports.title')}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-slate-400 mb-0 small">{t('reports.subtitle')}</p>
|
<p className="text-slate-400 mb-0" style={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}>
|
||||||
|
{t('reports.subtitle')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="card border-0 mb-4" style={{ background: '#0f172a' }}>
|
<div className="card border-0 mb-4" style={{ background: '#0f172a' }}>
|
||||||
<div className="card-body p-2">
|
<div className="card-body" style={{ padding: isMobile ? '0.5rem' : '0.75rem' }}>
|
||||||
<div className="d-flex flex-wrap gap-2">
|
<div className="d-flex flex-wrap gap-1">
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`btn btn-sm ${activeTab === tab.id ? 'btn-primary' : 'btn-outline-secondary'}`}
|
className={`btn ${activeTab === tab.id ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
style={{
|
||||||
|
fontSize: isMobile ? '0.7rem' : '0.875rem',
|
||||||
|
padding: isMobile ? '0.25rem 0.5rem' : '0.375rem 0.75rem'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<i className={`bi ${tab.icon} me-1`}></i>
|
<i className={`bi ${tab.icon} ${isMobile ? '' : 'me-1'}`}></i>
|
||||||
{tab.label}
|
{isMobile ? '' : tab.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user