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 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>

View File

@ -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>