diff --git a/backend/app/Http/Controllers/Api/TransactionController.php b/backend/app/Http/Controllers/Api/TransactionController.php index 0c51153..9d714b2 100755 --- a/backend/app/Http/Controllers/Api/TransactionController.php +++ b/backend/app/Http/Controllers/Api/TransactionController.php @@ -397,8 +397,13 @@ public function byWeek(Request $request): JsonResponse // Verificar se há filtros ativos (além de date_field e currency) $hasActiveFilters = $request->hasAny(['account_id', 'category_id', 'cost_center_id', 'type', 'status', 'search', 'start_date', 'end_date']); - // Se há filtros, trazer mais semanas para mostrar todos os resultados - $perPage = $hasActiveFilters ? 100 : $request->get('per_page', 10); // Mais semanas quando filtrado + // Verificar modo de visualização + $viewMode = $request->get('view_mode', 'week'); // 'week', 'month', 'all' + + // Determinar quantidade de semanas por página + // Se há filtros OU viewMode é 'month' ou 'all', trazer todas as semanas + $shouldFetchAll = $hasActiveFilters || in_array($viewMode, ['month', 'all']); + $perPage = $shouldFetchAll ? 1000 : $request->get('per_page', 10); $page = $request->get('page', 1); $currency = $request->get('currency'); // Filtro de divisa opcional $dateField = $request->get('date_field', 'planned_date'); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 6183fb0..fd2890b 100755 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -569,6 +569,13 @@ "amount": "Amount", "leaveEmptyForPlanned": "Leave empty to use planned amount", "week": "Week", + "weekly": "Weekly", + "monthly": "Monthly", + "all": "All", + "viewWeekly": "Weekly view", + "viewMonthly": "Monthly view", + "viewAll": "View all", + "allTransactions": "All Transactions", "type": { "label": "Type", "credit": "Credit", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 2436752..4947717 100755 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -577,6 +577,13 @@ "amount": "Valor", "leaveEmptyForPlanned": "Dejar vacío para usar el valor previsto", "week": "Semana", + "weekly": "Semanal", + "monthly": "Mensual", + "all": "Todas", + "viewWeekly": "Vista semanal", + "viewMonthly": "Vista mensual", + "viewAll": "Ver todas", + "allTransactions": "Todas las Transacciones", "type": { "label": "Tipo", "credit": "Crédito", diff --git a/frontend/src/i18n/locales/pt-BR.json b/frontend/src/i18n/locales/pt-BR.json index aa79885..5b91f3b 100755 --- a/frontend/src/i18n/locales/pt-BR.json +++ b/frontend/src/i18n/locales/pt-BR.json @@ -579,6 +579,13 @@ "amount": "Valor", "leaveEmptyForPlanned": "Deixe vazio para usar o valor previsto", "week": "Semana", + "weekly": "Semanal", + "monthly": "Mensal", + "all": "Todas", + "viewWeekly": "Visualização semanal", + "viewMonthly": "Visualização mensal", + "viewAll": "Ver todas", + "allTransactions": "Todas as Transações", "type": { "label": "Tipo", "credit": "Crédito", diff --git a/frontend/src/pages/TransactionsByWeek.jsx b/frontend/src/pages/TransactionsByWeek.jsx index 2d3fbf4..0d446fd 100755 --- a/frontend/src/pages/TransactionsByWeek.jsx +++ b/frontend/src/pages/TransactionsByWeek.jsx @@ -40,6 +40,9 @@ export default function Transactions() { // Estados de paginação const [page, setPage] = useState(1); const [perPage] = useState(5); // Semanas por página + + // Estado de visualização: 'week', 'month', 'all' + const [viewMode, setViewMode] = useState('week'); // Estados de filtro const [filters, setFilters] = useState({ @@ -212,6 +215,7 @@ export default function Transactions() { page, per_page: perPage, currency: selectedCurrency, + view_mode: viewMode, // Enviar modo de visualização para o backend }; // Remover params vazios @@ -253,7 +257,7 @@ export default function Transactions() { useEffect(() => { loadWeeklyData(); - }, [filters, page, selectedCurrency]); // eslint-disable-line react-hooks/exhaustive-deps + }, [filters, page, selectedCurrency, viewMode]); // eslint-disable-line react-hooks/exhaustive-deps // Mobile resize detection useEffect(() => { @@ -962,6 +966,12 @@ export default function Transactions() { } return `${startDay} ${startMonth} - ${endDay} ${endMonth}`; }; + + // Formatar nome do mês + const formatMonthName = (month) => { + const date = new Date(month.year, month.month - 1, 1); + return date.toLocaleDateString(getLocale(), { month: 'long', year: 'numeric' }); + }; // Funções de seleção de transações const getAllVisibleTransactionIds = () => { @@ -1060,8 +1070,56 @@ export default function Transactions() { const pagination = currentCurrencyData?.pagination; const weeks = currentCurrencyData?.weeks || []; - // Criar lista flat de todas as transações para modo filtrado + // Criar lista flat de todas as transações para modo filtrado ou "todas" const allTransactions = weeks.flatMap(week => week.transactions || []); + + // Agrupar transações por mês para viewMode 'month' + const months = React.useMemo(() => { + if (viewMode !== 'month' || hasActiveFilters) return []; + + const monthMap = {}; + allTransactions.forEach(txn => { + const date = txn.effective_date || txn.planned_date; + const monthKey = date.substring(0, 7); // "YYYY-MM" + if (!monthMap[monthKey]) { + monthMap[monthKey] = { + month_key: monthKey, + year: parseInt(monthKey.split('-')[0]), + month: parseInt(monthKey.split('-')[1]), + transactions: [], + summary: { + credits: { total: 0, count: 0 }, + debits: { total: 0, count: 0 }, + pending: { total: 0, count: 0 }, + total_transactions: 0, + balance: 0, + } + }; + } + monthMap[monthKey].transactions.push(txn); + monthMap[monthKey].summary.total_transactions++; + + const amount = txn.amount || txn.planned_amount; + if (txn.type === 'credit') { + monthMap[monthKey].summary.credits.total += amount; + monthMap[monthKey].summary.credits.count++; + } else { + monthMap[monthKey].summary.debits.total += amount; + monthMap[monthKey].summary.debits.count++; + } + if (txn.status === 'pending') { + monthMap[monthKey].summary.pending.total += amount; + monthMap[monthKey].summary.pending.count++; + } + }); + + // Calcular balance e ordenar + Object.values(monthMap).forEach(m => { + m.summary.balance = m.summary.credits.total - m.summary.debits.total; + }); + + return Object.values(monthMap).sort((a, b) => b.month_key.localeCompare(a.month_key)); + }, [allTransactions, viewMode, hasActiveFilters]); // Calcular totais gerais const totalStats = weeks.reduce((acc, week) => ({ @@ -1119,6 +1177,43 @@ export default function Transactions() { + {/* View Mode Selector + Stats */} +
+ {/* View Mode Buttons */} + {!hasActiveFilters && ( +
+ + + +
+ )} + {hasActiveFilters &&
} +
+ {/* Stats Cards */}
@@ -1554,8 +1649,8 @@ export default function Transactions() {
)} - {/* Weeks List (grouped view when NO filters are active) */} - {!loading && !hasActiveFilters && weeks.length > 0 && ( + {/* Weeks List (grouped view when NO filters are active and viewMode is 'week') */} + {!loading && !hasActiveFilters && viewMode === 'week' && weeks.length > 0 && (
{weeks.map((week) => (
@@ -2201,6 +2296,309 @@ export default function Transactions() {
)} + {/* Monthly List (grouped view when NO filters and viewMode is 'month') */} + {!loading && !hasActiveFilters && viewMode === 'month' && months.length > 0 && ( +
+ {months.map((month) => ( +
+ {/* Month Header */} +
toggleWeekExpansion(month.month_key)}> +
+
+ +
+
+

+ {formatMonthName(month)} +
+ {month.summary.total_transactions} +
+

+
+
+ + {/* Month Summary */} +
+
+
{t('transactions.credits')}
+
+{formatCurrency(month.summary.credits.total, selectedCurrency)}
+
+
+
{t('transactions.debits')}
+
-{formatCurrency(month.summary.debits.total, selectedCurrency)}
+
+
+
{t('transactions.balance')}
+
= 0 ? 'balance-pos' : 'balance-neg'}`}> + {month.summary.balance >= 0 ? '+' : ''}{formatCurrency(month.summary.balance, selectedCurrency)} +
+
+
+
+ + {/* Month Transactions */} + {expandedWeeks[month.month_key] && ( +
+ {isMobile ? ( + // Mobile: Cards Layout +
+ {month.transactions.map(transaction => ( +
+
+
+ + {formatDate(transaction.effective_date || transaction.planned_date)} + +
+ + {transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')} + +
+
+
openDetailModal(transaction)}> + + {transaction.description} + +
+
+ + + {transaction.account?.name} + + {transaction.category && ( + + + {transaction.category.name} + + )} +
+
+
+ {transaction.type === 'credit' ? '+' : '-'} + {formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)} +
+
+
+
+ ))} +
+ ) : ( + // Desktop: Table Layout + + + + + + + + + + + + + + + {month.transactions.map(transaction => ( + + + + + + + + + + + ))} + +
{t('transactions.date')}{t('transactions.description')}{t('transactions.account')}{t('transactions.category')}{t('transactions.amount')}{t('transactions.type.label')}{t('transactions.status.label')}
+ + {formatDate(transaction.effective_date || transaction.planned_date)} + + + openDetailModal(transaction)}> + {transaction.description} + + {transaction.account?.name} + {transaction.category && ( + + + {transaction.category.name} + + )} + + + {transaction.type === 'credit' ? '+' : '-'} + {formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)} + + + + {transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')} + + + + {t(`transactions.status.${transaction.status}`)} + + +
+ +
    +
  • + +
  • +
  • + +
  • +
+
+
+ )} +
+ )} +
+ ))} +
+ )} + + {/* All Transactions List (flat view when NO filters and viewMode is 'all') */} + {!loading && !hasActiveFilters && viewMode === 'all' && allTransactions.length > 0 && ( +
+
+
+
+
+ +
+
+

+ {t('transactions.allTransactions') || 'Todas as Transações'} +
+ {allTransactions.length} +
+

+
+
+ +
+
+
{t('transactions.credits')}
+
+{formatCurrency(totalStats.credits, selectedCurrency)}
+
+
+
{t('transactions.debits')}
+
-{formatCurrency(totalStats.debits, selectedCurrency)}
+
+
+
{t('transactions.balance')}
+
= 0 ? 'balance-pos' : 'balance-neg'}`}> + {(totalStats.credits - totalStats.debits) >= 0 ? '+' : ''}{formatCurrency(totalStats.credits - totalStats.debits, selectedCurrency)} +
+
+
+
+ +
+ {isMobile ? ( +
+ {allTransactions.map(transaction => ( +
+
+
+ + {formatDate(transaction.effective_date || transaction.planned_date)} + + + {transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')} + +
+
openDetailModal(transaction)}> + {transaction.description} +
+
+
+ {transaction.type === 'credit' ? '+' : '-'} + {formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)} +
+
+
+
+ ))} +
+ ) : ( + + + + + + + + + + + + + + + {allTransactions.map(transaction => ( + + + + + + + + + + + ))} + +
{t('transactions.date')}{t('transactions.description')}{t('transactions.account')}{t('transactions.category')}{t('transactions.amount')}{t('transactions.type.label')}{t('transactions.status.label')}
{formatDate(transaction.effective_date || transaction.planned_date)} openDetailModal(transaction)}>{transaction.description}{transaction.account?.name} + {transaction.category && ( + + + {transaction.category.name} + + )} + {transaction.type === 'credit' ? '+' : '-'}{formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)}{transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')}{t(`transactions.status.${transaction.status}`)} + +
+ )} +
+
+
+ )} + {/* Modal de Criar/Editar */}