diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee37d4..415475e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index ea67177..f8136f8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.43.6 +1.43.8 diff --git a/backend/app/Http/Controllers/Api/ReportController.php b/backend/app/Http/Controllers/Api/ReportController.php index a6ace8a..ae4578f 100644 --- a/backend/app/Http/Controllers/Api/ReportController.php +++ b/backend/app/Http/Controllers/Api/ReportController.php @@ -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' => [], diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index c0dd397..5fe704a 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -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", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 2b7cb07..a563677 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -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", diff --git a/frontend/src/i18n/locales/pt-BR.json b/frontend/src/i18n/locales/pt-BR.json index 673986a..4d95a50 100644 --- a/frontend/src/i18n/locales/pt-BR.json +++ b/frontend/src/i18n/locales/pt-BR.json @@ -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", diff --git a/frontend/src/pages/Reports.jsx b/frontend/src/pages/Reports.jsx index c2570d0..bc2fee3 100644 --- a/frontend/src/pages/Reports.jsx +++ b/frontend/src/pages/Reports.jsx @@ -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 (
| {t('reports.category')} | -{t('common.total')} | -% | -|||
|---|---|---|---|---|---|
| - - {cat.category_name} - | -{currency(cat.total, categoryData.currency)} | -- {cat.percentage}% - | + + {/* Desktop: Tabela */} + {!isMobile && ( +
| {t('reports.category')} | +{t('common.total')} | +% |
|---|
| # | -{t('reports.description')} | -{t('reports.category')} | -{t('reports.date')} | -{t('reports.amount')} | -% {t('reports.total')} | -||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| - - {i + 1} - - | -
-
- {item.description}
-
- |
- - {item.category || '-'} - | -{item.date} | -- - {currency(item.amount, item.currency || topExpenses.currency)} - - | -
-
-
-
+
+ {/* Desktop: Tabela */}
+ {!isMobile && (
+
+
+ )}
+
+ {/* Mobile: Cards compactos */}
+ {isMobile && (
+
+ {topExpenses.data.map((item, i) => {
+ const percentage = (item.amount / topExpenses.total) * 100;
+ return (
+
+
+ {/* Header: Ranking e Valor */}
+
+
+ #{i + 1}
+
+
+
-
+ {currency(item.amount, item.currency || topExpenses.currency)}
+
+
{percentage.toFixed(1)}%
|
-