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.
|
**IMPORTANTE:** Estamos trabalhando diretamente no servidor de produção.
|
||||||
|
|
||||||
- **Hostname:** mail.cnxifly.com
|
- **Hostname:** cnxifly.com
|
||||||
- **IP:** 213.165.93.60
|
- **IP:** 213.165.93.60
|
||||||
- **Sistema:** Ubuntu 24.04.3 LTS
|
- **Sistema:** Ubuntu 24.04.3 LTS
|
||||||
- **Workspace:** `/root/webmoney` (symlink: `/var/www/webmoney`)
|
- **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
|
cd /root/webmoney
|
||||||
git add -A && git commit -m "descrição" && git push origin main
|
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
|
### ❌ Proibições Git
|
||||||
|
|||||||
@ -393,7 +393,12 @@ public function duplicate(Request $request, Transaction $transaction): JsonRespo
|
|||||||
public function byWeek(Request $request): JsonResponse
|
public function byWeek(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$userId = $request->user()->id;
|
$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);
|
$page = $request->get('page', 1);
|
||||||
$currency = $request->get('currency'); // Filtro de divisa opcional
|
$currency = $request->get('currency'); // Filtro de divisa opcional
|
||||||
$dateField = $request->get('date_field', 'planned_date');
|
$dateField = $request->get('date_field', 'planned_date');
|
||||||
@ -432,11 +437,41 @@ public function byWeek(Request $request): JsonResponse
|
|||||||
$query->where('status', $request->status);
|
$query->where('status', $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtro por período
|
// Filtro por período - aceita filtros parciais
|
||||||
if ($request->has('start_date') && $request->has('end_date')) {
|
$hasDateFilter = $request->has('start_date') || $request->has('end_date');
|
||||||
$query->inPeriod($request->start_date, $request->end_date, $dateField);
|
|
||||||
} else {
|
// Para effective_date, usar COALESCE com planned_date como fallback
|
||||||
// Sem filtro de data: não mostrar transações futuras
|
$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());
|
$query->where('planned_date', '<=', now()->toDateString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -680,7 +680,10 @@
|
|||||||
"quickCostCenter": {
|
"quickCostCenter": {
|
||||||
"codePlaceholder": "E.g.: CC001",
|
"codePlaceholder": "E.g.: CC001",
|
||||||
"codeHelp": "Optional code to identify the cost center"
|
"codeHelp": "Optional code to identify the cost center"
|
||||||
}
|
},
|
||||||
|
"filteredResults": "Filtered Results",
|
||||||
|
"filterActive": "Filters active",
|
||||||
|
"uncategorized": "Uncategorized"
|
||||||
},
|
},
|
||||||
"currencies": {
|
"currencies": {
|
||||||
"BRL": "Brazilian Real",
|
"BRL": "Brazilian Real",
|
||||||
|
|||||||
@ -688,7 +688,10 @@
|
|||||||
"quickCostCenter": {
|
"quickCostCenter": {
|
||||||
"codePlaceholder": "Ej: CC001",
|
"codePlaceholder": "Ej: CC001",
|
||||||
"codeHelp": "Código opcional para identificar el centro de costo"
|
"codeHelp": "Código opcional para identificar el centro de costo"
|
||||||
}
|
},
|
||||||
|
"filteredResults": "Resultados Filtrados",
|
||||||
|
"filterActive": "Filtros activos",
|
||||||
|
"uncategorized": "Sin Categoría"
|
||||||
},
|
},
|
||||||
"currencies": {
|
"currencies": {
|
||||||
"BRL": "Real Brasileño",
|
"BRL": "Real Brasileño",
|
||||||
|
|||||||
@ -690,7 +690,10 @@
|
|||||||
"quickCostCenter": {
|
"quickCostCenter": {
|
||||||
"codePlaceholder": "Ex: CC001",
|
"codePlaceholder": "Ex: CC001",
|
||||||
"codeHelp": "Código opcional para identificar o centro de custo"
|
"codeHelp": "Código opcional para identificar o centro de custo"
|
||||||
}
|
},
|
||||||
|
"filteredResults": "Resultados Filtrados",
|
||||||
|
"filterActive": "Filtros ativos",
|
||||||
|
"uncategorized": "Sem Categoria"
|
||||||
},
|
},
|
||||||
"currencies": {
|
"currencies": {
|
||||||
"BRL": "Real Brasileiro",
|
"BRL": "Real Brasileiro",
|
||||||
|
|||||||
@ -1059,6 +1059,9 @@ export default function Transactions() {
|
|||||||
const currentCurrencyData = weeklyData?.data?.[selectedCurrency];
|
const currentCurrencyData = weeklyData?.data?.[selectedCurrency];
|
||||||
const pagination = currentCurrencyData?.pagination;
|
const pagination = currentCurrencyData?.pagination;
|
||||||
const weeks = currentCurrencyData?.weeks || [];
|
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
|
// Calcular totais gerais
|
||||||
const totalStats = weeks.reduce((acc, week) => ({
|
const totalStats = weeks.reduce((acc, week) => ({
|
||||||
@ -1301,8 +1304,258 @@ export default function Transactions() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Weeks List */}
|
{/* Filtered Transactions List (flat view when filters are active) */}
|
||||||
{!loading && weeks.length > 0 && (
|
{!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">
|
<div className="txn-weeks-container">
|
||||||
{weeks.map((week) => (
|
{weeks.map((week) => (
|
||||||
<div key={week.year_week} className={`txn-week ${expandedWeeks[week.year_week] ? 'expanded' : ''}`}>
|
<div key={week.year_week} className={`txn-week ${expandedWeeks[week.year_week] ? 'expanded' : ''}`}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user