feat: melhorias no filtro de transações
- Filtro 'Sem Categoria' mostra lista flat ao invés de agrupada por semanas - Transações futuras aparecem quando qualquer filtro está ativo - Filtro de data parcial (só data inicial ou só data final) - COALESCE para effective_date com fallback para planned_date - Traduções i18n para filteredResults, filterActive, uncategorized
This commit is contained in:
parent
f2e032f002
commit
44bc999840
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@ -4,7 +4,7 @@
|
||||
|
||||
**IMPORTANTE:** Estamos trabalhando diretamente no servidor de produção.
|
||||
|
||||
- **Hostname:** mail.cnxifly.com
|
||||
- **Hostname:** cnxifly.com
|
||||
- **IP:** 213.165.93.60
|
||||
- **Sistema:** Ubuntu 24.04.3 LTS
|
||||
- **Workspace:** `/root/webmoney` (symlink: `/var/www/webmoney`)
|
||||
@ -55,7 +55,7 @@ cd /root/webmoney/backend && php artisan config:clear && php artisan cache:clear
|
||||
cd /root/webmoney
|
||||
git add -A && git commit -m "descrição" && git push origin main
|
||||
|
||||
# 6. Atualizar VERSION e CHANGELOG.md se necessário
|
||||
# 6. Atualizar VERSION, README, APRENDIZEDOS_TECNICOS e CHANGELOG.md se necessário
|
||||
```
|
||||
|
||||
### ❌ Proibições Git
|
||||
|
||||
@ -393,7 +393,12 @@ public function duplicate(Request $request, Transaction $transaction): JsonRespo
|
||||
public function byWeek(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
$perPage = $request->get('per_page', 10); // Semanas por página
|
||||
|
||||
// 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
|
||||
$page = $request->get('page', 1);
|
||||
$currency = $request->get('currency'); // Filtro de divisa opcional
|
||||
$dateField = $request->get('date_field', 'planned_date');
|
||||
@ -432,11 +437,41 @@ public function byWeek(Request $request): JsonResponse
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Filtro por período
|
||||
if ($request->has('start_date') && $request->has('end_date')) {
|
||||
$query->inPeriod($request->start_date, $request->end_date, $dateField);
|
||||
} else {
|
||||
// Sem filtro de data: não mostrar transações futuras
|
||||
// Filtro por período - aceita filtros parciais
|
||||
$hasDateFilter = $request->has('start_date') || $request->has('end_date');
|
||||
|
||||
// Para effective_date, usar COALESCE com planned_date como fallback
|
||||
$dateColumn = $dateField === 'effective_date'
|
||||
? DB::raw('COALESCE(effective_date, planned_date)')
|
||||
: $dateField;
|
||||
|
||||
if ($hasDateFilter) {
|
||||
if ($request->has('start_date') && $request->has('end_date')) {
|
||||
// Ambas as datas especificadas
|
||||
if ($dateField === 'effective_date') {
|
||||
$query->whereRaw('COALESCE(effective_date, planned_date) >= ?', [$request->start_date])
|
||||
->whereRaw('COALESCE(effective_date, planned_date) <= ?', [$request->end_date]);
|
||||
} else {
|
||||
$query->inPeriod($request->start_date, $request->end_date, $dateField);
|
||||
}
|
||||
} elseif ($request->has('start_date')) {
|
||||
// Apenas data inicial - mostrar a partir desta data
|
||||
if ($dateField === 'effective_date') {
|
||||
$query->whereRaw('COALESCE(effective_date, planned_date) >= ?', [$request->start_date]);
|
||||
} else {
|
||||
$query->where($dateField, '>=', $request->start_date);
|
||||
}
|
||||
} elseif ($request->has('end_date')) {
|
||||
// Apenas data final - mostrar até esta data (incluindo futuras)
|
||||
if ($dateField === 'effective_date') {
|
||||
$query->whereRaw('COALESCE(effective_date, planned_date) <= ?', [$request->end_date]);
|
||||
} else {
|
||||
$query->where($dateField, '<=', $request->end_date);
|
||||
}
|
||||
}
|
||||
} elseif (!$hasActiveFilters) {
|
||||
// Sem filtro de data E sem filtros ativos: não mostrar transações futuras
|
||||
// Quando há filtros ativos, mostrar todas as transações (incluindo futuras)
|
||||
$query->where('planned_date', '<=', now()->toDateString());
|
||||
}
|
||||
|
||||
|
||||
@ -680,7 +680,10 @@
|
||||
"quickCostCenter": {
|
||||
"codePlaceholder": "E.g.: CC001",
|
||||
"codeHelp": "Optional code to identify the cost center"
|
||||
}
|
||||
},
|
||||
"filteredResults": "Filtered Results",
|
||||
"filterActive": "Filters active",
|
||||
"uncategorized": "Uncategorized"
|
||||
},
|
||||
"currencies": {
|
||||
"BRL": "Brazilian Real",
|
||||
|
||||
@ -688,7 +688,10 @@
|
||||
"quickCostCenter": {
|
||||
"codePlaceholder": "Ej: CC001",
|
||||
"codeHelp": "Código opcional para identificar el centro de costo"
|
||||
}
|
||||
},
|
||||
"filteredResults": "Resultados Filtrados",
|
||||
"filterActive": "Filtros activos",
|
||||
"uncategorized": "Sin Categoría"
|
||||
},
|
||||
"currencies": {
|
||||
"BRL": "Real Brasileño",
|
||||
|
||||
@ -690,7 +690,10 @@
|
||||
"quickCostCenter": {
|
||||
"codePlaceholder": "Ex: CC001",
|
||||
"codeHelp": "Código opcional para identificar o centro de custo"
|
||||
}
|
||||
},
|
||||
"filteredResults": "Resultados Filtrados",
|
||||
"filterActive": "Filtros ativos",
|
||||
"uncategorized": "Sem Categoria"
|
||||
},
|
||||
"currencies": {
|
||||
"BRL": "Real Brasileiro",
|
||||
|
||||
@ -1060,6 +1060,9 @@ export default function Transactions() {
|
||||
const pagination = currentCurrencyData?.pagination;
|
||||
const weeks = currentCurrencyData?.weeks || [];
|
||||
|
||||
// Criar lista flat de todas as transações para modo filtrado
|
||||
const allTransactions = weeks.flatMap(week => week.transactions || []);
|
||||
|
||||
// Calcular totais gerais
|
||||
const totalStats = weeks.reduce((acc, week) => ({
|
||||
credits: acc.credits + (week.summary?.credits?.total || 0),
|
||||
@ -1301,8 +1304,258 @@ export default function Transactions() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Weeks List */}
|
||||
{!loading && weeks.length > 0 && (
|
||||
{/* Filtered Transactions List (flat view when filters are active) */}
|
||||
{!loading && hasActiveFilters && allTransactions.length > 0 && (
|
||||
<div className="txn-weeks-container">
|
||||
<div className="txn-week expanded">
|
||||
{/* Header mostrando info do filtro */}
|
||||
<div className="txn-week-header" style={{ cursor: 'default' }}>
|
||||
<div className="txn-week-left">
|
||||
<div className="txn-week-chevron">
|
||||
<i className="bi bi-funnel text-warning"></i>
|
||||
</div>
|
||||
<div className="txn-week-info">
|
||||
<h3>
|
||||
{t('transactions.filteredResults') || 'Resultados Filtrados'}
|
||||
<div className="txn-week-badges">
|
||||
<span className="txn-week-badge count">{allTransactions.length}</span>
|
||||
</div>
|
||||
</h3>
|
||||
<div className="dates text-warning">
|
||||
<i className="bi bi-funnel me-1"></i>
|
||||
{t('transactions.filterActive') || 'Filtros ativos'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary totals */}
|
||||
<div className="txn-week-summary">
|
||||
<div className="txn-week-stat">
|
||||
<div className="txn-week-stat-label">{t('transactions.credits')}</div>
|
||||
<div className="txn-week-stat-value credit">+{formatCurrency(totalStats.credits, selectedCurrency)}</div>
|
||||
</div>
|
||||
<div className="txn-week-stat">
|
||||
<div className="txn-week-stat-label">{t('transactions.debits')}</div>
|
||||
<div className="txn-week-stat-value debit">-{formatCurrency(totalStats.debits, selectedCurrency)}</div>
|
||||
</div>
|
||||
<div className="txn-week-stat">
|
||||
<div className="txn-week-stat-label">{t('transactions.balance')}</div>
|
||||
<div className={`txn-week-stat-value ${(totalStats.credits - totalStats.debits) >= 0 ? 'balance-pos' : 'balance-neg'}`}>
|
||||
{(totalStats.credits - totalStats.debits) >= 0 ? '+' : ''}{formatCurrency(totalStats.credits - totalStats.debits, selectedCurrency)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All transactions in flat list */}
|
||||
<div className="txn-week-body">
|
||||
{isMobile ? (
|
||||
// Mobile: Cards Layout
|
||||
<div className="d-flex flex-column gap-2 p-2">
|
||||
{allTransactions.map(transaction => (
|
||||
<div
|
||||
key={transaction.id}
|
||||
ref={transaction.id === highlightedTransactionId ? highlightedRef : null}
|
||||
className={`card border-secondary ${transaction.is_overdue ? 'border-danger' : ''} ${transaction.id === highlightedTransactionId ? 'border-primary' : ''} ${selectedTransactionIds.has(transaction.id) ? 'bg-primary bg-opacity-10' : ''}`}
|
||||
style={{ background: '#0f172a', cursor: 'pointer' }}
|
||||
>
|
||||
<div className="card-body p-3">
|
||||
{/* Header: Date + Type Badge + Status */}
|
||||
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={selectedTransactionIds.has(transaction.id)}
|
||||
onChange={() => handleToggleTransaction(transaction.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-slate-400" style={{ fontSize: '0.75rem' }}>
|
||||
{formatDate(transaction.effective_date || transaction.planned_date)}
|
||||
{transaction.is_overdue && <i className="bi bi-exclamation-triangle text-danger ms-1"></i>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<span className={`badge ${transaction.type === 'credit' ? 'bg-success' : 'bg-danger'} bg-opacity-25 ${transaction.type === 'credit' ? 'text-success' : 'text-danger'}`} style={{ fontSize: '0.65rem' }}>
|
||||
{transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')}
|
||||
</span>
|
||||
<span className={`badge ${
|
||||
transaction.status === 'completed' ? 'bg-success' :
|
||||
transaction.status === 'pending' ? 'bg-warning' :
|
||||
transaction.status === 'cancelled' ? 'bg-secondary' : 'bg-info'
|
||||
} bg-opacity-25 text-${
|
||||
transaction.status === 'completed' ? 'success' :
|
||||
transaction.status === 'pending' ? 'warning' :
|
||||
transaction.status === 'cancelled' ? 'secondary' : 'info'
|
||||
}`} style={{ fontSize: '0.65rem' }}>
|
||||
{t(`transactions.status.${transaction.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-2" onClick={() => openDetailModal(transaction)}>
|
||||
<div className="d-flex align-items-center gap-1 mb-1">
|
||||
{transaction.is_transfer && <i className="bi bi-arrow-left-right text-info"></i>}
|
||||
{transaction.is_reconciled && <i className="bi bi-link-45deg text-success" title={t('transactions.reconciled')}></i>}
|
||||
<span className="text-white fw-medium" style={{ fontSize: '0.85rem' }}>
|
||||
{transaction.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account + Category */}
|
||||
<div className="d-flex flex-wrap gap-2 mb-2">
|
||||
<span className="badge bg-secondary" style={{ fontSize: '0.7rem' }}>
|
||||
<i className="bi bi-wallet2 me-1"></i>
|
||||
{transaction.account?.name}
|
||||
</span>
|
||||
{transaction.category && (
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
backgroundColor: transaction.category.color + '20',
|
||||
color: transaction.category.color,
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
>
|
||||
<i className={`bi ${transaction.category.icon} me-1`}></i>
|
||||
{transaction.category.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Amount + Actions */}
|
||||
<div className="d-flex justify-content-between align-items-center pt-2" style={{ borderTop: '1px solid #334155' }}>
|
||||
<div className={`fw-bold ${transaction.type === 'credit' ? 'text-success' : 'text-danger'}`} style={{ fontSize: '1rem' }}>
|
||||
{transaction.type === 'credit' ? '+' : '-'}
|
||||
{formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => openQuickCategorizeModal(transaction)}
|
||||
>
|
||||
<i className="bi bi-tags"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Desktop: Table Layout
|
||||
<table className="txn-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '40px' }} className="text-center col-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={allTransactions.length > 0 && allTransactions.every(t => selectedTransactionIds.has(t.id))}
|
||||
onChange={() => {
|
||||
const allIds = allTransactions.map(t => t.id);
|
||||
const allSelected = allIds.every(id => selectedTransactionIds.has(id));
|
||||
setSelectedTransactionIds(prev => {
|
||||
const next = new Set(prev);
|
||||
allIds.forEach(id => allSelected ? next.delete(id) : next.add(id));
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th style={{ width: '90px' }} className="col-date">{t('transactions.date')}</th>
|
||||
<th className="col-description">{t('transactions.description')}</th>
|
||||
<th style={{ width: '120px' }} className="col-account">{t('transactions.account')}</th>
|
||||
<th style={{ width: '140px' }} className="col-category">{t('transactions.category')}</th>
|
||||
<th style={{ width: '110px' }} className="text-end col-amount">{t('transactions.amount')}</th>
|
||||
<th style={{ width: '70px' }} className="text-center col-type">{t('transactions.type.label')}</th>
|
||||
<th style={{ width: '80px' }} className="text-center col-status">{t('transactions.status.label')}</th>
|
||||
<th style={{ width: '40px' }} className="text-center col-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allTransactions.map(transaction => (
|
||||
<tr
|
||||
key={transaction.id}
|
||||
ref={transaction.id === highlightedTransactionId ? highlightedRef : null}
|
||||
className={`${transaction.is_overdue ? 'overdue' : ''} ${transaction.id === highlightedTransactionId ? 'highlighted-transaction' : ''} ${selectedTransactionIds.has(transaction.id) ? 'selected-row' : ''}`}
|
||||
>
|
||||
<td className="text-center col-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={selectedTransactionIds.has(transaction.id)}
|
||||
onChange={() => handleToggleTransaction(transaction.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="col-date">
|
||||
<span className="txn-date">
|
||||
{formatDate(transaction.effective_date || transaction.planned_date)}
|
||||
{transaction.is_overdue && <i className="bi bi-exclamation-triangle overdue-icon"></i>}
|
||||
</span>
|
||||
</td>
|
||||
<td className="col-description">
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
{transaction.is_transfer && <span className="txn-transfer-badge"><i className="bi bi-arrow-left-right"></i></span>}
|
||||
{transaction.is_reconciled && (
|
||||
<span className="txn-reconciled-badge" title={t('transactions.reconciled')}>
|
||||
<i className="bi bi-link-45deg"></i>
|
||||
</span>
|
||||
)}
|
||||
<span className="txn-description" onClick={() => openDetailModal(transaction)}>
|
||||
{transaction.description}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="col-account"><span className="txn-account">{transaction.account?.name}</span></td>
|
||||
<td className="col-category">
|
||||
{transaction.category && (
|
||||
<span
|
||||
className="txn-category-badge"
|
||||
style={{ backgroundColor: transaction.category.color + '20', color: transaction.category.color }}
|
||||
>
|
||||
<i className={`bi ${transaction.category.icon}`}></i>
|
||||
{transaction.category.name}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="col-amount">
|
||||
<span className={`txn-amount ${transaction.type}`}>
|
||||
{transaction.type === 'credit' ? '+' : '-'}
|
||||
{formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center col-type">
|
||||
<span className={`txn-type-badge ${transaction.type}`}>
|
||||
{transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center col-status">
|
||||
<span className={`txn-status-badge ${transaction.status}`}>
|
||||
{t(`transactions.status.${transaction.status}`)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center col-actions">
|
||||
<button
|
||||
className="txn-actions-btn"
|
||||
onClick={() => openQuickCategorizeModal(transaction)}
|
||||
title={t('transactions.quickCategorize')}
|
||||
>
|
||||
<i className="bi bi-tags"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Weeks List (grouped view when NO filters are active) */}
|
||||
{!loading && !hasActiveFilters && weeks.length > 0 && (
|
||||
<div className="txn-weeks-container">
|
||||
{weeks.map((week) => (
|
||||
<div key={week.year_week} className={`txn-week ${expandedWeeks[week.year_week] ? 'expanded' : ''}`}>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user