v1.43.8 - Drill-down de subcategorias em relatórios

ADDED:
- Relatório Por Categoria agora suporta drill-down clicável
- Clicar em categoria pai exibe gráfico com suas subcategorias
- Botão Voltar para retornar às categorias principais
- Hover visual em linhas de tabela e cards clicáveis
- Backend aceita parent_id como parâmetro em /reports/by-category

CHANGED:
- Backend: query padrão mostra apenas categorias pai (parent_id IS NULL)
- Backend: nova query para subcategorias quando parent_id é fornecido
- Frontend: estado selectedCategory para rastrear navegação
- Frontend: onClick handlers em gráfico, tabela e cards

TRANSLATION:
- pt-BR: Distribuição de Subcategorias
- es: Distribución de Subcategorías
- en: Subcategory Distribution
This commit is contained in:
marcoitaloesp-ai 2025-12-16 15:07:44 +00:00 committed by GitHub
parent b0724d7b2c
commit 7b9345dc80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 362 additions and 124 deletions

View File

@ -5,6 +5,48 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.43.8] - 2025-12-16
### Added
- **Relatório de Categorias - Drill-down para Subcategorias**
- Clicar em uma categoria pai agora mostra suas subcategorias em um novo gráfico
- Mesmo layout visual (gráfico de pizza + lista/cards)
- Botão "Voltar" para retornar às categorias principais
- Hover visual em tabelas e cards clicáveis
- Backend suporta filtro `parent_id` para mostrar apenas subcategorias de uma categoria específica
- **Tradução i18n**
- pt-BR: "Distribuição de Subcategorias"
- es: "Distribución de Subcategorías"
- en: "Subcategory Distribution"
### Changed
- **Backend - API /reports/by-category**
- Novo parâmetro `parent_id` para filtrar subcategorias
- Query padrão agora mostra apenas categorias pai (`parent_id IS NULL`)
- Query com `parent_id` filtra subcategorias da categoria especificada
## [1.43.7] - 2025-12-16
### Changed
- **Backend - Relatório de Centros de Custo incluindo transações sem centro**
- Transações sem centro de custo (`cost_center_id = NULL`) agora aparecem como categoria "General"
- Cor padrão: #6b7280 para transações não classificadas
- Query modificada: COALESCE para tratar NULLs como id=0, nome='General'
- Relatório completo: todas as transações aparecem no relatório byCostCenter
### Added
- **Frontend - Tradução i18n 'general'**
- pt-BR: "Geral"
- es: "General"
- en: "General"
- **Frontend - Reports Mobile Optimization (Continuação)**
- **Tab Category**: col-12 mobile, gráfico 300px, lista em cards compactos
- **Tab Evolution**: Cards 2x2, gráficos 250px/200px, botões xs, fontes 0.7-0.85rem
- **Tab TopExpenses - Lista Detalhada**: Cards compactos mobile vs tabela desktop
## [1.43.6] - 2025-12-16
### Improved

View File

@ -1 +1 @@
1.43.6
1.43.8

View File

@ -150,9 +150,33 @@ public function byCategory(Request $request)
$endDate = $request->get('end_date', now()->format('Y-m-d'));
$type = $request->get('type', 'debit');
$groupByParent = $request->get('group_by_parent', false);
$parentId = $request->get('parent_id');
// Se filtrar por parent_id, mostra subcategorias dessa categoria pai
if ($parentId) {
$data = DB::select("
SELECT
c.id as category_id,
c.name as category_name,
c.icon,
c.color,
COALESCE(a.currency, 'EUR') as currency,
SUM(ABS(t.amount)) as total
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
LEFT JOIN accounts a ON t.account_id = a.id
WHERE t.user_id = ?
AND t.effective_date BETWEEN ? AND ?
AND t.type = ?
AND c.parent_id = ?
AND t.deleted_at IS NULL
AND {$this->excludeTransfers()}
GROUP BY c.id, c.name, c.icon, c.color, COALESCE(a.currency, 'EUR')
ORDER BY total DESC
", [$this->userId, $startDate, $endDate, $type, $parentId]);
}
// Si se quiere agrupar por categoría padre, obtenemos el nombre del padre
if ($groupByParent) {
else if ($groupByParent) {
$data = DB::select("
SELECT
COALESCE(c.parent_id, c.id) as category_id,
@ -174,7 +198,7 @@ public function byCategory(Request $request)
ORDER BY total DESC
", [$this->userId, $startDate, $endDate, $type]);
} else {
// Sin agrupar: cada subcategoría se muestra individualmente
// Sin agrupar: solo categorías padre (sin parent_id)
$data = DB::select("
SELECT
c.id as category_id,
@ -189,6 +213,7 @@ public function byCategory(Request $request)
WHERE t.user_id = ?
AND t.effective_date BETWEEN ? AND ?
AND t.type = ?
AND c.parent_id IS NULL
AND t.deleted_at IS NULL
AND {$this->excludeTransfers()}
GROUP BY c.id, c.name, c.icon, c.color, COALESCE(a.currency, 'EUR')
@ -244,9 +269,9 @@ public function byCostCenter(Request $request)
$data = DB::select("
SELECT
cc.id as cost_center_id,
cc.name as cost_center_name,
cc.color,
COALESCE(cc.id, 0) as cost_center_id,
COALESCE(cc.name, 'Sem classificar') as cost_center_name,
COALESCE(cc.color, '#6b7280') as color,
COALESCE(a.currency, 'EUR') as currency,
SUM(CASE WHEN t.type = 'credit' THEN t.amount ELSE 0 END) as income,
SUM(CASE WHEN t.type = 'debit' THEN ABS(t.amount) ELSE 0 END) as expense
@ -257,8 +282,7 @@ public function byCostCenter(Request $request)
AND t.effective_date BETWEEN ? AND ?
AND t.deleted_at IS NULL
AND {$this->excludeTransfers()}
AND t.cost_center_id IS NOT NULL
GROUP BY cc.id, cc.name, cc.color, COALESCE(a.currency, 'EUR')
GROUP BY COALESCE(cc.id, 0), COALESCE(cc.name, 'Sem classificar'), COALESCE(cc.color, '#6b7280'), COALESCE(a.currency, 'EUR')
ORDER BY expense DESC
", [$this->userId, $startDate, $endDate]);
@ -268,9 +292,9 @@ public function byCostCenter(Request $request)
$ccId = $row->cost_center_id;
if (!isset($byCostCenter[$ccId])) {
$byCostCenter[$ccId] = [
'id' => $ccId,
'id' => $ccId == 0 ? null : $ccId,
'name' => $row->cost_center_name,
'color' => $row->color ?? '#6b7280',
'color' => $row->color,
'income_converted' => 0,
'expense_converted' => 0,
'by_currency' => [],

View File

@ -53,6 +53,7 @@
"items": "items",
"difference": "Difference",
"months": "months",
"unclassified": "Unclassified",
"viewAll": "View all",
"today": "Today",
"selectTransactions": "Select transactions",
@ -1856,6 +1857,7 @@
"canImprove": "Can improve",
"total": "Total",
"expenseDistribution": "Expense Distribution",
"subcategoryDistribution": "Subcategory Distribution",
"categoryDetail": "Category Detail",
"category": "Category",
"amount": "Amount",

View File

@ -54,6 +54,7 @@
"items": "elementos",
"difference": "Diferencia",
"months": "meses",
"unclassified": "Sin clasificar",
"viewAll": "Ver todos",
"today": "Hoy",
"selectTransactions": "Seleccionar transacciones",
@ -1843,6 +1844,8 @@
"vsAverage": "vs promedio histórico",
"yearComparison": "Comparativa Anual",
"expenseDistribution": "Distribución de Gastos",
"subcategoryDistribution": "Distribución de Subcategorías",
"subcategoryDistribution": "Distribución de Subcategorías",
"categoryDetail": "Detalle por Categoría",
"category": "Categoría",
"amount": "Monto",

View File

@ -54,6 +54,7 @@
"items": "itens",
"difference": "Diferença",
"months": "meses",
"unclassified": "Sem classificar",
"viewAll": "Ver todos",
"date": "Data",
"today": "Hoje",
@ -1862,6 +1863,7 @@
"canImprove": "Pode melhorar",
"total": "Total",
"expenseDistribution": "Distribuição de Despesas",
"subcategoryDistribution": "Distribuição de Subcategorias",
"categoryDetail": "Detalhes por Categoria",
"category": "Categoria",
"amount": "Valor",

View File

@ -52,6 +52,7 @@ const Reports = () => {
// Data states
const [summary, setSummary] = useState(null);
const [categoryData, setCategoryData] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null);
const [evolutionData, setEvolutionData] = useState(null);
const [dayOfWeekData, setDayOfWeekData] = useState(null);
const [topExpenses, setTopExpenses] = useState(null);
@ -73,7 +74,11 @@ const Reports = () => {
setSummary(summaryRes);
break;
case 'category':
const catRes = await reportService.getByCategory({ type: 'debit' });
const params = { type: 'debit' };
if (selectedCategory) {
params.parent_id = selectedCategory.category_id;
}
const catRes = await reportService.getByCategory(params);
setCategoryData(catRes);
break;
case 'evolution':
@ -440,17 +445,40 @@ const Reports = () => {
`hsl(${(i * 360) / categoryData.data.length}, 70%, 50%)`
);
const handleCategoryClick = (cat) => {
setSelectedCategory(cat);
loadData();
};
const handleBackToCategories = () => {
setSelectedCategory(null);
loadData();
};
return (
<div className="row g-4">
<div className="col-lg-6">
{/* Botão Voltar */}
{selectedCategory && (
<div className="col-12">
<button
className="btn btn-sm btn-outline-secondary"
onClick={handleBackToCategories}
>
<i className="bi bi-arrow-left me-2"></i>
{t('common.back')} - {selectedCategory.category_name}
</button>
</div>
)}
{/* Gráfico de Distribuição */}
<div className={isMobile ? 'col-12' : 'col-lg-6'}>
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<div className={`card-header border-0 bg-transparent ${isMobile ? 'p-2' : ''}`}>
<h6 className="text-white mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
<i className="bi bi-pie-chart me-2"></i>
{t('reports.expenseDistribution')}
{selectedCategory ? t('reports.subcategoryDistribution') : t('reports.expenseDistribution')}
</h6>
</div>
<div className="card-body" style={{ height: '400px' }}>
<div className={isMobile ? 'p-2' : 'card-body'} style={{ height: isMobile ? '300px' : '400px' }}>
<Doughnut
data={{
labels: categoryData.data.map(c => c.category_name),
@ -460,46 +488,100 @@ const Reports = () => {
borderWidth: 0,
}],
}}
options={doughnutOptions}
options={{
...doughnutOptions,
onClick: !selectedCategory ? (event, elements) => {
if (elements.length > 0) {
const index = elements[0].index;
const cat = categoryData.data[index];
handleCategoryClick(cat);
}
} : undefined
}}
/>
</div>
</div>
</div>
<div className="col-lg-6">
{/* Tabela/Lista de Detalhes */}
<div className={isMobile ? 'col-12' : 'col-lg-6'}>
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent d-flex justify-content-between">
<h6 className="text-white mb-0">
<div className={`card-header border-0 bg-transparent d-flex justify-content-between ${isMobile ? 'p-2' : ''}`}>
<h6 className="text-white mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
<i className="bi bi-list-ol me-2"></i>
{t('reports.categoryDetail')}
</h6>
<span className="text-success fw-bold">{currency(categoryData.total, categoryData.currency)}</span>
<span className="text-success fw-bold" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
{currency(categoryData.total, categoryData.currency)}
</span>
</div>
<div className="card-body p-0" style={{ maxHeight: '400px', overflowY: 'auto' }}>
<table className="table table-dark table-hover mb-0">
<thead className="sticky-top" style={{ background: '#1e293b' }}>
<tr>
<th>{t('reports.category')}</th>
<th className="text-end">{t('common.total')}</th>
<th className="text-end">%</th>
</tr>
</thead>
<tbody>
{categoryData.data.map((cat, i) => (
<tr key={cat.category_id}>
<td>
<i className={`bi ${cat.icon} me-2`} style={{ color: colors[i] }}></i>
{cat.category_name}
</td>
<td className="text-end">{currency(cat.total, categoryData.currency)}</td>
<td className="text-end">
<span className="badge bg-secondary">{cat.percentage}%</span>
</td>
{/* Desktop: Tabela */}
{!isMobile && (
<div className="card-body p-0" style={{ maxHeight: '400px', overflowY: 'auto' }}>
<table className="table table-dark table-hover mb-0">
<thead className="sticky-top" style={{ background: '#1e293b' }}>
<tr>
<th>{t('reports.category')}</th>
<th className="text-end">{t('common.total')}</th>
<th className="text-end">%</th>
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{categoryData.data.map((cat, i) => (
<tr
key={cat.category_id}
onClick={() => !selectedCategory && handleCategoryClick(cat)}
style={{ cursor: !selectedCategory ? 'pointer' : 'default' }}
className={!selectedCategory ? 'table-hover-row' : ''}
>
<td>
<i className={`bi ${cat.icon} me-2`} style={{ color: colors[i] }}></i>
{cat.category_name}
</td>
<td className="text-end">{currency(cat.total, categoryData.currency)}</td>
<td className="text-end">
<span className="badge bg-secondary">{cat.percentage}%</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Mobile: Cards */}
{isMobile && (
<div className="p-2" style={{ maxHeight: '400px', overflowY: 'auto' }}>
{categoryData.data.map((cat, i) => (
<div
key={cat.category_id}
className="card mb-2 border-0"
style={{ background: '#1e293b', cursor: !selectedCategory ? 'pointer' : 'default' }}
onClick={() => !selectedCategory && handleCategoryClick(cat)}
>
<div className="card-body p-2">
<div className="d-flex justify-content-between align-items-start mb-1">
<div className="d-flex align-items-center gap-2">
<i className={`bi ${cat.icon}`} style={{ color: colors[i], fontSize: '1rem' }}></i>
<span className="text-white" style={{ fontSize: '0.85rem' }}>
{cat.category_name}
</span>
</div>
<span className="badge bg-secondary" style={{ fontSize: '0.7rem' }}>
{cat.percentage}%
</span>
</div>
<div className="text-end">
<span className="text-success fw-bold" style={{ fontSize: '0.9rem' }}>
{currency(cat.total, categoryData.currency)}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
@ -512,13 +594,15 @@ const Reports = () => {
return (
<div className="row g-4">
{/* Period Selector */}
<div className="col-12">
<div className="d-flex gap-2 mb-3">
<div className={`d-flex gap-2 ${isMobile ? 'mb-2' : 'mb-3'}`}>
{[6, 12, 24].map(m => (
<button
key={m}
className={`btn btn-sm ${months === m ? 'btn-primary' : 'btn-outline-secondary'}`}
className={`btn ${isMobile ? 'btn-xs' : 'btn-sm'} ${months === m ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setMonths(m)}
style={isMobile ? { fontSize: '0.75rem', padding: '0.25rem 0.5rem' } : {}}
>
{m} {t('common.months')}
</button>
@ -527,37 +611,51 @@ const Reports = () => {
</div>
{/* Averages Cards */}
<div className="col-md-3">
<div className={isMobile ? 'col-6' : 'col-md-3'}>
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.avgIncome')}</small>
<h5 className="text-success mb-0">{currency(evolutionData.averages.income, evolutionData.currency)}</h5>
<div className={isMobile ? 'card-body p-2' : 'card-body'}>
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
{t('reports.avgIncome')}
</small>
<h5 className="text-success mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1.25rem' }}>
{currency(evolutionData.averages.income, evolutionData.currency)}
</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className={isMobile ? 'col-6' : 'col-md-3'}>
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.avgExpense')}</small>
<h5 className="text-danger mb-0">{currency(evolutionData.averages.expense, evolutionData.currency)}</h5>
<div className={isMobile ? 'card-body p-2' : 'card-body'}>
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
{t('reports.avgExpense')}
</small>
<h5 className="text-danger mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1.25rem' }}>
{currency(evolutionData.averages.expense, evolutionData.currency)}
</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className={isMobile ? 'col-6' : 'col-md-3'}>
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.balance')}</small>
<h5 className={`mb-0 ${evolutionData.averages.balance >= 0 ? 'text-success' : 'text-danger'}`}>
<div className={isMobile ? 'card-body p-2' : 'card-body'}>
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
{t('reports.balance')}
</small>
<h5 className={`mb-0 ${evolutionData.averages.balance >= 0 ? 'text-success' : 'text-danger'}`} style={{ fontSize: isMobile ? '0.85rem' : '1.25rem' }}>
{currency(evolutionData.averages.balance, evolutionData.currency)}
</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className={isMobile ? 'col-6' : 'col-md-3'}>
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.savingsRate')}</small>
<h5 className="text-primary mb-0">{evolutionData.averages.savings_rate}%</h5>
<div className={isMobile ? 'card-body p-2' : 'card-body'}>
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
{t('reports.savingsRate')}
</small>
<h5 className="text-primary mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1.25rem' }}>
{evolutionData.averages.savings_rate}%
</h5>
</div>
</div>
</div>
@ -565,13 +663,13 @@ const Reports = () => {
{/* Evolution Chart */}
<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">
<div className={`card-header border-0 bg-transparent ${isMobile ? 'p-2' : ''}`}>
<h6 className="text-white mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
<i className="bi bi-graph-up me-2"></i>
{t('reports.monthlyEvolution')}
</h6>
</div>
<div className="card-body" style={{ height: '350px' }}>
<div className={isMobile ? 'p-2' : 'card-body'} style={{ height: isMobile ? '250px' : '350px' }}>
<Line
data={{
labels: evolutionData.data.map(d => d.month_label),
@ -611,13 +709,13 @@ const Reports = () => {
{/* Savings Rate Chart */}
<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">
<div className={`card-header border-0 bg-transparent ${isMobile ? 'p-2' : ''}`}>
<h6 className="text-white mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
<i className="bi bi-percent me-2"></i>
{t('reports.savingsRate')} por mes
</h6>
</div>
<div className="card-body" style={{ height: '250px' }}>
<div className={isMobile ? 'p-2' : 'card-body'} style={{ height: isMobile ? '200px' : '250px' }}>
<Bar
data={{
labels: evolutionData.data.map(d => d.month_label),
@ -1073,71 +1171,130 @@ const Reports = () => {
</div>
</div>
{/* Tabela Detalhada */}
{/* Tabela/Lista Detalhada */}
<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">
<div className={`card-header border-0 bg-transparent ${isMobile ? 'p-2' : ''}`}>
<h6 className="text-white mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
<i className="bi bi-table me-2"></i>
{t('reports.detailedList')} ({topExpenses.data.length} {t('reports.transactions')})
</h6>
</div>
<div className="card-body p-0">
<div className="table-responsive" style={{ maxHeight: '500px' }}>
<table className="table table-dark table-hover mb-0">
<thead className="sticky-top" style={{ background: '#1e293b' }}>
<tr>
<th className="text-center" style={{ width: '60px' }}>#</th>
<th>{t('reports.description')}</th>
<th>{t('reports.category')}</th>
<th className="text-center">{t('reports.date')}</th>
<th className="text-end">{t('reports.amount')}</th>
<th className="text-center" style={{ width: '100px' }}>% {t('reports.total')}</th>
</tr>
</thead>
<tbody>
{topExpenses.data.map((item, i) => {
const percentage = (item.amount / topExpenses.total) * 100;
return (
<tr key={item.id}>
<td className="text-center">
<span className={`badge ${i < 3 ? 'bg-danger' : i < 10 ? 'bg-warning text-dark' : 'bg-secondary'}`}>
{i + 1}
</span>
</td>
<td>
<div className="text-white text-truncate" style={{ maxWidth: '300px' }}>
{item.description}
</div>
</td>
<td>
<span className="badge bg-primary">{item.category || '-'}</span>
</td>
<td className="text-center text-slate-400 small">{item.date}</td>
<td className="text-end">
<span className="text-danger fw-bold">
{currency(item.amount, item.currency || topExpenses.currency)}
</span>
</td>
<td className="text-center">
<div className="d-flex align-items-center justify-content-center gap-2">
<div className="progress flex-grow-1" style={{ height: '6px', width: '50px' }}>
<div
className="progress-bar bg-danger"
style={{ width: `${percentage}%` }}
></div>
<div className={isMobile ? 'p-2' : 'card-body p-0'}>
{/* Desktop: Tabela */}
{!isMobile && (
<div className="table-responsive" style={{ maxHeight: '500px' }}>
<table className="table table-dark table-hover mb-0">
<thead className="sticky-top" style={{ background: '#1e293b' }}>
<tr>
<th className="text-center" style={{ width: '60px' }}>#</th>
<th>{t('reports.description')}</th>
<th>{t('reports.category')}</th>
<th className="text-center">{t('reports.date')}</th>
<th className="text-end">{t('reports.amount')}</th>
<th className="text-center" style={{ width: '100px' }}>% {t('reports.total')}</th>
</tr>
</thead>
<tbody>
{topExpenses.data.map((item, i) => {
const percentage = (item.amount / topExpenses.total) * 100;
return (
<tr key={item.id}>
<td className="text-center">
<span className={`badge ${i < 3 ? 'bg-danger' : i < 10 ? 'bg-warning text-dark' : 'bg-secondary'}`}>
{i + 1}
</span>
</td>
<td>
<div className="text-white text-truncate" style={{ maxWidth: '300px' }}>
{item.description}
</div>
<small className="text-slate-400" style={{ minWidth: '45px' }}>
</td>
<td>
<span className="badge bg-primary">{item.category || '-'}</span>
</td>
<td className="text-center text-slate-400 small">{item.date}</td>
<td className="text-end">
<span className="text-danger fw-bold">
{currency(item.amount, item.currency || topExpenses.currency)}
</span>
</td>
<td className="text-center">
<div className="d-flex align-items-center justify-content-center gap-2">
<div className="progress flex-grow-1" style={{ height: '6px', width: '50px' }}>
<div
className="progress-bar bg-danger"
style={{ width: `${percentage}%` }}
></div>
</div>
<small className="text-slate-400" style={{ minWidth: '45px' }}>
{percentage.toFixed(1)}%
</small>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* Mobile: Cards compactos */}
{isMobile && (
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
{topExpenses.data.map((item, i) => {
const percentage = (item.amount / topExpenses.total) * 100;
return (
<div
key={item.id}
className="card mb-2 border-0"
style={{ background: '#1e293b' }}
>
<div className="card-body p-2">
{/* Header: Ranking e Valor */}
<div className="d-flex justify-content-between align-items-start mb-2">
<span className={`badge ${i < 3 ? 'bg-danger' : i < 10 ? 'bg-warning text-dark' : 'bg-secondary'}`} style={{ fontSize: '0.75rem' }}>
#{i + 1}
</span>
<div className="text-end">
<div className="text-danger fw-bold" style={{ fontSize: '0.95rem' }}>
{currency(item.amount, item.currency || topExpenses.currency)}
</div>
<small className="text-slate-400" style={{ fontSize: '0.7rem' }}>
{percentage.toFixed(1)}%
</small>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Descrição */}
<div className="text-white mb-1" style={{ fontSize: '0.85rem', lineHeight: '1.3' }}>
{item.description}
</div>
{/* Footer: Categoria e Data */}
<div className="d-flex justify-content-between align-items-center">
<span className="badge bg-primary" style={{ fontSize: '0.7rem' }}>
{item.category || '-'}
</span>
<small className="text-slate-400" style={{ fontSize: '0.7rem' }}>
{item.date}
</small>
</div>
{/* Progress bar */}
<div className="progress mt-2" style={{ height: '4px' }}>
<div
className="progress-bar bg-danger"
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
@ -1800,6 +1957,14 @@ const Reports = () => {
return (
<div className="reports-container">
{/* CSS para hover na tabela */}
<style>{`
.table-hover-row:hover {
background-color: rgba(59, 130, 246, 0.1) !important;
transition: background-color 0.2s ease;
}
`}</style>
{/* Header */}
<div className={`d-flex ${isMobile ? 'flex-column gap-2' : 'justify-content-between align-items-center'} mb-4`}>
<div>