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:
marcoitaloesp-ai 2025-12-16 13:20:02 +00:00 committed by GitHub
parent 1637e5da0c
commit b0724d7b2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 96 additions and 50 deletions

View File

@ -250,8 +250,8 @@ const Categories = () => {
const renderCategory = (category, level = 0, parentColor = null) => {
const hasChildren = category.children && category.children.length > 0;
const isExpanded = expandedCategories[category.id];
// Subcategorias herdam a cor da categoria pai
const displayColor = level > 0 && parentColor ? parentColor : category.color;
// 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 || '#94a3b8');
return (
<div key={category.id}>
@ -343,7 +343,7 @@ const Categories = () => {
{/* Children */}
{hasChildren && isExpanded && (
<div>
{category.children.map(child => renderCategory(child, level + 1, category.color))}
{category.children.map(child => renderCategory(child, level + 1, displayColor))}
</div>
)}
</div>

View File

@ -35,6 +35,15 @@ const Reports = () => {
const { t } = useTranslation();
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 [loading, setLoading] = useState(true);
const [year, setYear] = useState(new Date().getFullYear());
@ -178,11 +187,11 @@ const Reports = () => {
const monthlyAvgBalance = (summary.current.balance / 12).toFixed(2);
return (
<div className="row g-4">
<div className="row g-3">
{/* Year Selector */}
<div className="col-12">
<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>
{t('reports.annualSummary')}
</h5>
@ -190,8 +199,12 @@ const Reports = () => {
{[2024, 2025].map(y => (
<button
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)}
style={{
fontSize: isMobile ? '0.75rem' : '0.875rem',
padding: isMobile ? '0.25rem 0.75rem' : '0.375rem 1rem'
}}
>
{y}
</button>
@ -201,29 +214,38 @@ const Reports = () => {
</div>
{/* 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-body text-white">
<div className="d-flex align-items-center justify-content-between mb-3">
<div>
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.income')}</div>
<h3 className="mb-0 fw-bold">{currency(summary.current.income, summary.currency)}</h3>
</div>
<div className="fs-1 opacity-50">
<i className="bi bi-arrow-up-circle"></i>
<div className="card-body text-white" style={{ padding: isMobile ? '0.75rem' : '1rem' }}>
<div className="d-flex align-items-center justify-content-between mb-2">
<div style={{ minWidth: 0, flex: 1 }}>
<div className="opacity-75 text-uppercase fw-semibold mb-1" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
{t('reports.income')}
</div>
<h3 className="mb-0 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
{currency(summary.current.income, summary.currency)}
</h3>
</div>
{!isMobile && (
<div className="fs-1 opacity-50">
<i className="bi bi-arrow-up-circle"></i>
</div>
)}
</div>
{summary.variation.income !== 0 && !isNaN(summary.variation.income) && isFinite(summary.variation.income) && (
<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>
{Math.abs(summary.variation.income).toFixed(1)}%
</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>
)}
<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="fw-semibold">{currency(monthlyAvgIncome, summary.currency)}</span>
</div>
@ -231,29 +253,38 @@ const Reports = () => {
</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-body text-white">
<div className="d-flex align-items-center justify-content-between mb-3">
<div>
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.expenses')}</div>
<h3 className="mb-0 fw-bold">{currency(summary.current.expense, summary.currency)}</h3>
</div>
<div className="fs-1 opacity-50">
<i className="bi bi-arrow-down-circle"></i>
<div className="card-body text-white" style={{ padding: isMobile ? '0.75rem' : '1rem' }}>
<div className="d-flex align-items-center justify-content-between mb-2">
<div style={{ minWidth: 0, flex: 1 }}>
<div className="opacity-75 text-uppercase fw-semibold mb-1" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
{t('reports.expenses')}
</div>
<h3 className="mb-0 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
{currency(summary.current.expense, summary.currency)}
</h3>
</div>
{!isMobile && (
<div className="fs-1 opacity-50">
<i className="bi bi-arrow-down-circle"></i>
</div>
)}
</div>
{summary.variation.expense !== 0 && !isNaN(summary.variation.expense) && isFinite(summary.variation.expense) && (
<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>
{Math.abs(summary.variation.expense).toFixed(1)}%
</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>
)}
<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="fw-semibold">{currency(monthlyAvgExpense, summary.currency)}</span>
</div>
@ -261,29 +292,38 @@ const Reports = () => {
</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-body text-white">
<div className="d-flex align-items-center justify-content-between mb-3">
<div>
<div className="opacity-75 small text-uppercase fw-semibold mb-1">{t('reports.balance')}</div>
<h3 className="mb-0 fw-bold">{currency(summary.current.balance, summary.currency)}</h3>
</div>
<div className="fs-1 opacity-50">
<i className="bi bi-wallet2"></i>
<div className="card-body text-white" style={{ padding: isMobile ? '0.75rem' : '1rem' }}>
<div className="d-flex align-items-center justify-content-between mb-2">
<div style={{ minWidth: 0, flex: 1 }}>
<div className="opacity-75 text-uppercase fw-semibold mb-1" style={{ fontSize: isMobile ? '0.65rem' : '0.75rem' }}>
{t('reports.balance')}
</div>
<h3 className="mb-0 fw-bold" style={{ fontSize: isMobile ? '1.1rem' : '1.5rem' }}>
{currency(summary.current.balance, summary.currency)}
</h3>
</div>
{!isMobile && (
<div className="fs-1 opacity-50">
<i className="bi bi-wallet2"></i>
</div>
)}
</div>
{summary.variation.balance !== 0 && !isNaN(summary.variation.balance) && isFinite(summary.variation.balance) && (
<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>
{Math.abs(summary.variation.balance).toFixed(1)}%
</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>
)}
<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="fw-semibold">{currency(monthlyAvgBalance, summary.currency)}</span>
</div>
@ -1761,28 +1801,34 @@ const Reports = () => {
return (
<div className="reports-container">
{/* 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>
<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>
{t('reports.title')}
</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>
{/* Tabs */}
<div className="card border-0 mb-4" style={{ background: '#0f172a' }}>
<div className="card-body p-2">
<div className="d-flex flex-wrap gap-2">
<div className="card-body" style={{ padding: isMobile ? '0.5rem' : '0.75rem' }}>
<div className="d-flex flex-wrap gap-1">
{tabs.map(tab => (
<button
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)}
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>
{tab.label}
<i className={`bi ${tab.icon} ${isMobile ? '' : 'me-1'}`}></i>
{isMobile ? '' : tab.label}
</button>
))}
</div>