feat: accounts list — active-only filter, date range & as-on-date search, modern UI

- Show only active accounts (status=300) for SB/CA/FD/RD lists; status sent as query param
- Add FROM/TO date range filter (API fires only when both are set) and independent AS ON DATE filter
- Restrict date pickers to today max (no future date selection)
- Default sort: latest accounts on top (ORDER BY id DESC)
- Full UI redesign: accent header, monospace account pill, client avatar, modern badges
- AccountsService: added status, fromDate, toDate, asOnDate to AccountListParams and all 4 methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-21 13:53:58 +05:30
co-authored by Claude Sonnet 4.6
parent 45d4397b0e
commit 846b2484d7
4 changed files with 570 additions and 348 deletions
@@ -9,71 +9,105 @@
<div class="acl-page">
<!-- Header -->
<div class="acl-header" [ngClass]="config.accentClass">
<div class="acl-header__left">
<div class="acl-header__icon-wrap">
<fa-icon [icon]="config.icon"></fa-icon>
</div>
<div>
<h2 class="acl-header__title">{{ config.label }}</h2>
<span class="acl-header__subtitle">
@if (!isLoading) {
{{ totalRecords }} total records
} @else {
Loading...
}
</span>
</div>
<div class="acl-header__icon">
<fa-icon [icon]="config.icon"></fa-icon>
</div>
<div class="acl-header__text">
<h2 class="acl-header__title">{{ config.label }}</h2>
<span class="acl-header__count">
@if (isLoading) {
Loading...
} @else {
{{ totalRecords }} active accounts
}
</span>
</div>
</div>
<!-- Toolbar -->
<div class="acl-toolbar">
<div class="acl-search-field">
<fa-icon icon="search" class="acl-search-field__icon"></fa-icon>
<!-- Search Bar -->
<div class="acl-search-wrap">
<div class="acl-search">
<fa-icon icon="search" class="acl-search__icon"></fa-icon>
<input
[formControl]="searchControl"
class="acl-search-field__input"
[placeholder]="searchType === 'accountNo' ? 'Search by account number...' : 'Search by client name...'"
class="acl-search__input"
[placeholder]="searchType === 'accountNo' ? 'Search by account number…' : 'Search by client name…'"
/>
@if (searchControl.value) {
<button class="acl-search-field__clear" (click)="clearSearch()" matTooltip="Clear">
<button class="acl-search__clear" (click)="clearSearch()">
<fa-icon icon="times"></fa-icon>
</button>
}
</div>
<button
class="acl-toggle-btn"
class="acl-btn acl-btn--toggle"
(click)="toggleSearchType()"
[matTooltip]="'Switch to ' + (searchType === 'accountNo' ? 'client name' : 'account number') + ' search'"
>
<fa-icon icon="exchange-alt"></fa-icon>
{{ searchType === 'accountNo' ? 'Acct No' : 'Client' }}
</button>
<button class="acl-refresh-btn" (click)="loadAccounts()" matTooltip="Refresh">
<button class="acl-btn acl-btn--icon" (click)="loadAccounts()" matTooltip="Refresh">
<fa-icon icon="sync-alt"></fa-icon>
</button>
</div>
<!-- Table Card -->
<div class="acl-table-card">
<!-- Date Filters -->
<div class="acl-filters">
<div class="acl-filters__group">
<div class="acl-datepick">
<span class="acl-datepick__label">From</span>
<input type="date" class="acl-datepick__input" [formControl]="fromDateControl" [max]="today" />
</div>
<div class="acl-filters__arrow">
<fa-icon icon="arrow-right"></fa-icon>
</div>
<div class="acl-datepick">
<span class="acl-datepick__label">To</span>
<input type="date" class="acl-datepick__input" [formControl]="toDateControl" [max]="today" />
</div>
</div>
<div class="acl-filters__sep"></div>
<div class="acl-datepick acl-datepick--ason">
<span class="acl-datepick__label">
<fa-icon icon="clock"></fa-icon>
As On Date
</span>
<input type="date" class="acl-datepick__input" [formControl]="asOnDateControl" [max]="today" />
</div>
@if (hasDateFilters) {
<button class="acl-filters__clear" (click)="clearDateFilters()">
<fa-icon icon="times"></fa-icon>
Clear
</button>
}
</div>
<!-- Table -->
<div class="acl-card">
@if (isLoading) {
<div class="acl-loading">
<mat-spinner diameter="36"></mat-spinner>
<span>Loading accounts...</span>
<div class="acl-state acl-state--loading">
<mat-spinner diameter="32"></mat-spinner>
<span>Loading accounts…</span>
</div>
}
@if (!isLoading && dataSource.data.length === 0) {
<div class="acl-empty">
<fa-icon icon="inbox" size="2x" class="acl-empty__icon"></fa-icon>
<p>No accounts found</p>
<div class="acl-state acl-state--empty">
<div class="acl-state__icon">
<fa-icon icon="inbox"></fa-icon>
</div>
<p class="acl-state__title">No active accounts found</p>
<p class="acl-state__sub">Try adjusting your search</p>
</div>
}
@if (!isLoading && dataSource.data.length > 0) {
<div class="acl-table-wrapper">
<div class="acl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)" class="acl-table">
<!-- Account No -->
<ng-container matColumnDef="accountNo">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Account No</th>
<td mat-cell *matCellDef="let row" class="acl-td">
@@ -81,33 +115,30 @@
</td>
</ng-container>
<!-- Type -->
<ng-container matColumnDef="depositType">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--center">Type</th>
<td mat-cell *matCellDef="let row" class="acl-td acl-td--center">
<span class="acl-dtype-badge" [ngClass]="getDepositTypeBadgeClass(row)">
{{ getDepositTypeLabel(row) }}
</span>
<span class="acl-type-badge" [ngClass]="config.typeBadgeClass">{{ config.typeLabel }}</span>
</td>
</ng-container>
<!-- Client -->
<ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Client</th>
<td mat-cell *matCellDef="let row" class="acl-td">
<span class="acl-client-name">{{ row.clientName }}</span>
<div class="acl-client">
<div class="acl-client__avatar">{{ (row.clientName || '?')[0] }}</div>
<span class="acl-client__name">{{ row.clientName }}</span>
</div>
</td>
</ng-container>
<!-- Product -->
<ng-container matColumnDef="productName">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Product</th>
<th mat-header-cell *matHeaderCellDef class="acl-th">Product</th>
<td mat-cell *matCellDef="let row" class="acl-td">
<span class="acl-product">{{ row.savingsProductName || row.depositProductName || row.productName }}</span>
</td>
</ng-container>
<!-- Balance -->
<ng-container matColumnDef="balance">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--right">Balance</th>
<td mat-cell *matCellDef="let row" class="acl-td acl-td--right">
@@ -115,37 +146,24 @@
</td>
</ng-container>
<!-- Status -->
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--center">Status</th>
<td mat-cell *matCellDef="let row" class="acl-td acl-td--center">
<span class="acl-status-badge" [ngClass]="getStatusClass(row)">
{{ getStatusLabel(row) }}
</span>
<span class="acl-status" [ngClass]="getStatusClass(row)">{{ getStatusLabel(row) }}</span>
</td>
</ng-container>
<!-- Actions -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--center">Actions</th>
<td mat-cell *matCellDef="let row" class="acl-td acl-td--center">
<button
class="acl-view-btn"
(click)="$event.stopPropagation(); viewAccount(row)"
matTooltip="View Account"
>
<button class="acl-view-btn" (click)="$event.stopPropagation(); viewAccount(row)" matTooltip="View">
<fa-icon icon="eye"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns" class="acl-header-row"></tr>
<tr
mat-row
*matRowDef="let row; columns: displayedColumns"
class="acl-data-row"
(click)="viewAccount(row)"
></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="acl-row" (click)="viewAccount(row)"></tr>
</table>
</div>
@@ -6,18 +6,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// ─── Tokens ───────────────────────────────────────────────────────────────────
$radius-sm: 8px;
$radius-md: 12px;
$radius-lg: 16px;
$shadow-card:
0 1px 3px rgb(0 0 0 / 6%),
0 4px 16px rgb(0 0 0 / 6%);
$transition: 0.15s ease;
$t: 0.18s ease;
$r-sm: 10px;
$r-md: 14px;
$r-lg: 18px;
$shadow:
0 1px 3px rgb(0 0 0 / 5%),
0 4px 20px rgb(0 0 0 / 6%);
$shadow-sm: 0 1px 3px rgb(0 0 0 / 8%);
// ─── Page ─────────────────────────────────────────────────────────────────────
.acl-page {
padding: 1.25rem 1.5rem;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
@@ -29,114 +29,106 @@ $transition: 0.15s ease;
.acl-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.25rem 1.5rem;
border-radius: $radius-lg;
background: #fff;
box-shadow: $shadow-card;
border-radius: $r-lg;
box-shadow: $shadow;
position: relative;
overflow: hidden;
&::before {
&::after {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: 5px;
background: var(--accent-color, #1976d2);
border-radius: $radius-lg 0 0 $radius-lg;
width: 4px;
background: var(--ac, #1976d2);
border-radius: $r-lg 0 0 $r-lg;
}
&__left {
display: flex;
align-items: center;
gap: 1rem;
}
&__icon-wrap {
&__icon {
width: 48px;
height: 48px;
border-radius: $radius-md;
background: var(--accent-bg, #e3f2fd);
border-radius: $r-md;
background: var(--ac-bg, #e3f2fd);
color: var(--ac, #1976d2);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.35rem;
color: var(--accent-color, #1976d2);
font-size: 1.3rem;
flex-shrink: 0;
}
&__title {
margin: 0;
font-size: 1.1rem;
margin: 0 0 2px;
font-size: 1.05rem;
font-weight: 700;
color: #1a2332;
color: #0f172a;
letter-spacing: -0.01em;
line-height: 1.3;
}
&__subtitle {
font-size: 0.73rem;
&__count {
font-size: 0.72rem;
color: #94a3b8;
margin-top: 3px;
display: block;
font-weight: 500;
}
// ── Accent variants ────────────────────────────────────────────────────────
// accent variants
&.accent--saving {
--accent-color: #1565c0;
--accent-bg: #dbeafe;
--ac: #1565c0;
--ac-bg: #dbeafe;
}
&.accent--current {
--accent-color: #065f46;
--accent-bg: #d1fae5;
--ac: #065f46;
--ac-bg: #d1fae5;
}
&.accent--fd {
--accent-color: #6b21a8;
--accent-bg: #f3e8ff;
--ac: #6b21a8;
--ac-bg: #f3e8ff;
}
&.accent--rd {
--accent-color: #c2410c;
--accent-bg: #ffedd5;
--ac: #c2410c;
--ac-bg: #ffedd5;
}
}
// ─── Toolbar (Search + Actions) ───────────────────────────────────────────────
.acl-toolbar {
// ─── Search Bar ───────────────────────────────────────────────────────────────
.acl-search-wrap {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
gap: 0.6rem;
background: #fff;
border-radius: $radius-md;
box-shadow: $shadow-card;
border-radius: $r-md;
box-shadow: $shadow;
padding: 0.6rem 0.75rem;
}
.acl-search-field {
.acl-search {
flex: 1;
display: flex;
align-items: center;
gap: 0.6rem;
background: #f8fafc;
border: 1.5px solid #e2e8f0;
border-radius: $radius-sm;
padding: 0.45rem 0.9rem;
border-radius: $r-sm;
padding: 0.42rem 0.85rem;
transition:
border-color $transition,
box-shadow $transition;
border-color $t,
box-shadow $t,
background $t;
&:focus-within {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgb(59 130 246 / 12%);
box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
background: #fff;
}
&__icon {
color: #94a3b8;
font-size: 0.8rem;
font-size: 0.78rem;
flex-shrink: 0;
}
@@ -159,9 +151,9 @@ $transition: 0.15s ease;
color: #cbd5e1;
cursor: pointer;
padding: 0;
font-size: 0.75rem;
font-size: 0.72rem;
transition: color $t;
line-height: 1;
transition: color $transition;
&:hover {
color: #64748b;
@@ -169,183 +161,343 @@ $transition: 0.15s ease;
}
}
.acl-toggle-btn,
.acl-refresh-btn {
white-space: nowrap;
padding: 0.45rem 0.9rem;
border-radius: $radius-sm;
border: 1.5px solid #e2e8f0;
background: #f8fafc;
font-size: 0.78rem;
font-weight: 600;
color: #475569;
cursor: pointer;
.acl-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.42rem 0.9rem;
border-radius: $r-sm;
border: 1.5px solid #e2e8f0;
background: #f8fafc;
font-size: 0.77rem;
font-weight: 600;
color: #475569;
cursor: pointer;
white-space: nowrap;
transition:
background $transition,
color $transition,
border-color $transition;
background $t,
color $t,
border-color $t;
&:hover {
background: #eff6ff;
color: #1d4ed8;
border-color: #bfdbfe;
}
&--icon {
width: 38px;
height: 38px;
padding: 0;
justify-content: center;
flex-shrink: 0;
}
}
.acl-refresh-btn {
width: 38px;
height: 38px;
padding: 0;
justify-content: center;
flex-shrink: 0;
}
// ─── Table Card ───────────────────────────────────────────────────────────────
.acl-table-card {
background: #fff;
border-radius: $radius-lg;
box-shadow: $shadow-card;
border: 1px solid #f1f5f9;
overflow: hidden;
min-height: 280px;
position: relative;
}
.acl-loading {
// ─── Date Filters ─────────────────────────────────────────────────────────────
.acl-filters {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 4rem;
color: #94a3b8;
font-size: 0.83rem;
font-weight: 500;
}
.acl-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 4rem;
color: #cbd5e1;
background: #fff;
border-radius: $r-md;
box-shadow: $shadow;
padding: 0.65rem 1rem;
flex-wrap: wrap;
&__group {
display: flex;
align-items: center;
gap: 0.4rem;
}
&__arrow {
color: #cbd5e1;
font-size: 0.72rem;
flex-shrink: 0;
}
&__sep {
width: 1px;
height: 26px;
background: #e2e8f0;
margin: 0 0.4rem;
flex-shrink: 0;
}
&__clear {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.34rem 0.85rem;
border-radius: 20px;
border: 1.5px solid #fecdd3;
background: #fff1f2;
color: #be123c;
font-size: 0.72rem;
font-weight: 700;
cursor: pointer;
transition:
background $t,
border-color $t,
box-shadow $t;
white-space: nowrap;
&:hover {
background: #ffe4e6;
border-color: #fda4af;
box-shadow: 0 0 0 3px rgb(253 164 175 / 15%);
}
}
}
.acl-datepick {
display: flex;
flex-direction: column;
gap: 0.15rem;
&__label {
font-size: 0.59rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.08em;
display: flex;
align-items: center;
gap: 0.2rem;
padding-left: 2px;
fa-icon {
font-size: 0.57rem;
}
}
&__input {
border: 1.5px solid #e2e8f0;
border-radius: $r-sm;
background: #f8fafc;
padding: 0.3rem 0.6rem;
font-size: 0.77rem;
color: #1e293b;
outline: none;
cursor: pointer;
min-width: 128px;
transition:
border-color $t,
background $t,
box-shadow $t;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
background: #fff;
}
&::-webkit-calendar-picker-indicator {
opacity: 0.35;
cursor: pointer;
font-size: 0.72rem;
&:hover {
opacity: 0.75;
}
}
}
&--ason {
.acl-datepick__label {
color: #6366f1;
}
.acl-datepick__input {
border-color: #c7d2fe;
background: #eef2ff;
&:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgb(99 102 241 / 10%);
background: #fff;
}
}
}
}
// ─── Card ─────────────────────────────────────────────────────────────────────
.acl-card {
background: #fff;
border-radius: $r-lg;
box-shadow: $shadow;
overflow: hidden;
min-height: 260px;
}
// ─── States ───────────────────────────────────────────────────────────────────
.acl-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.6rem;
padding: 4rem 2rem;
&--loading {
color: #94a3b8;
font-size: 0.83rem;
font-weight: 500;
flex-direction: row;
gap: 0.75rem;
}
&__icon {
color: #e2e8f0;
width: 56px;
height: 56px;
border-radius: 50%;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
color: #cbd5e1;
}
p {
font-size: 0.88rem;
font-weight: 500;
color: #94a3b8;
&__title {
margin: 0;
font-size: 0.92rem;
font-weight: 600;
color: #475569;
}
}
.acl-table-wrapper {
overflow-x: auto;
&__sub {
margin: 0;
font-size: 0.78rem;
color: #94a3b8;
}
}
// ─── Table ────────────────────────────────────────────────────────────────────
.acl-table-wrap {
overflow-x: auto;
}
.acl-table {
width: 100%;
.acl-header-row {
background: #f8fafc;
border-bottom: 2px solid #e2e8f0;
border-bottom: 1.5px solid #f1f5f9;
}
.acl-th {
font-size: 0.65rem;
font-weight: 700;
font-size: 0.63rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #64748b;
padding: 0.75rem 1rem;
letter-spacing: 0.08em;
color: #94a3b8;
padding: 0.85rem 1rem;
white-space: nowrap;
&--right {
text-align: right !important;
}
border-bottom: none;
&--center {
text-align: center !important;
}
&--right {
text-align: right !important;
}
}
.acl-data-row {
.acl-row {
cursor: pointer;
border-bottom: 1px solid #f8fafc;
transition: background $transition;
&:hover {
background: #f0f7ff !important;
.acl-acct-no {
color: #1d4ed8;
background: #dbeafe;
}
}
transition: background $t;
&:last-child {
border-bottom: none;
}
&:hover {
background: #fafbff !important;
.acl-acct-no {
background: #dbeafe;
color: #1d4ed8;
}
}
}
.acl-td {
padding: 0.7rem 1rem;
padding: 0.75rem 1rem;
font-size: 0.82rem;
vertical-align: middle;
&--right {
text-align: right !important;
}
border-bottom: none;
&--center {
text-align: center !important;
}
&--right {
text-align: right !important;
}
}
}
// ─── Cell Atoms ───────────────────────────────────────────────────────────────
.acl-acct-no {
font-family: 'Courier New', monospace;
font-size: 0.77rem;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.76rem;
font-weight: 700;
color: #1565c0;
color: #1d4ed8;
background: #eff6ff;
padding: 3px 10px;
padding: 4px 12px;
border-radius: 20px;
white-space: nowrap;
transition:
background $transition,
color $transition;
background $t,
color $t;
letter-spacing: 0.3px;
}
.acl-client-name {
font-weight: 600;
color: #1e293b;
font-size: 0.82rem;
.acl-client {
display: flex;
align-items: center;
gap: 0.6rem;
&__avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-size: 0.7rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text-transform: uppercase;
}
&__name {
font-weight: 600;
color: #1e293b;
font-size: 0.83rem;
}
}
.acl-product {
font-size: 0.75rem;
font-size: 0.74rem;
color: #64748b;
background: #f1f5f9;
padding: 3px 9px;
border-radius: $radius-sm;
border: 1px solid #e2e8f0;
padding: 3px 10px;
border-radius: $r-sm;
display: inline-block;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
border: 1px solid #e2e8f0;
}
.acl-balance {
@@ -356,16 +508,16 @@ $transition: 0.15s ease;
font-variant-numeric: tabular-nums;
}
// ─── Deposit Type Badge ───────────────────────────────────────────────────────
.acl-dtype-badge {
// ─── Type Badge ───────────────────────────────────────────────────────────────
.acl-type-badge {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.62rem;
font-weight: 800;
font-size: 0.6rem;
font-weight: 900;
padding: 3px 10px;
border-radius: 20px;
letter-spacing: 0.6px;
letter-spacing: 0.8px;
text-transform: uppercase;
&.dtype--sb {
@@ -394,14 +546,14 @@ $transition: 0.15s ease;
}
// ─── Status Badge ─────────────────────────────────────────────────────────────
.acl-status-badge {
.acl-status {
display: inline-block;
font-size: 0.65rem;
font-weight: 700;
padding: 3px 10px;
font-size: 0.63rem;
font-weight: 800;
padding: 3px 11px;
border-radius: 20px;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.6px;
white-space: nowrap;
&.status--submitted {
@@ -447,11 +599,11 @@ $transition: 0.15s ease;
}
}
// ─── Action Button ────────────────────────────────────────────────────────────
// ─── View Button ──────────────────────────────────────────────────────────────
.acl-view-btn {
width: 32px;
height: 32px;
border-radius: $radius-sm;
border-radius: $r-sm;
border: 1.5px solid #e2e8f0;
background: #f8fafc;
color: #64748b;
@@ -461,10 +613,10 @@ $transition: 0.15s ease;
justify-content: center;
font-size: 0.78rem;
transition:
background $transition,
color $transition,
border-color $transition,
box-shadow $transition;
background $t,
color $t,
border-color $t,
box-shadow $t;
&:hover {
background: #eff6ff;
@@ -479,4 +631,9 @@ $transition: 0.15s ease;
border-top: 1px solid #f1f5f9;
background: #fafbfe;
font-size: 0.8rem;
::ng-deep .mat-mdc-paginator-container {
padding: 0.4rem 1rem;
min-height: 52px;
}
}
@@ -24,14 +24,12 @@ import {
MatRowDef,
MatRow
} from '@angular/material/table';
import { Subject } from 'rxjs';
import { Subject, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { NgClass, NgIf } from '@angular/common';
import { NgClass } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatCardModule } from '@angular/material/card';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatButtonModule } from '@angular/material/button';
import { FormatNumberPipe } from '../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { AccountsService } from '../accounts.service';
@@ -41,8 +39,9 @@ export type AccountType = 'savings' | 'current' | 'fixed-deposits' | 'recurring-
interface AccountConfig {
label: string;
icon: string;
color: string;
accentClass: string;
typeLabel: string;
typeBadgeClass: string;
}
@Component({
@@ -68,11 +67,8 @@ interface AccountConfig {
MatRow,
MatPaginator,
NgClass,
NgIf,
MatCardModule,
MatTooltipModule,
MatProgressSpinnerModule,
MatButtonModule,
FormatNumberPipe
]
})
@@ -80,7 +76,6 @@ export class AccountsListComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute);
private router = inject(Router);
private accountsService = inject(AccountsService);
private destroy$ = new Subject<void>();
accountType: AccountType = 'savings';
@@ -102,24 +97,41 @@ export class AccountsListComponent implements OnInit, OnDestroy {
searchControl = new FormControl('');
searchType: 'accountNo' | 'clientName' = 'accountNo';
fromDateControl = new FormControl('');
toDateControl = new FormControl('');
asOnDateControl = new FormControl('');
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
readonly configs: Record<AccountType, AccountConfig> = {
savings: { label: 'Savings Accounts', icon: 'piggy-bank', color: '#1976d2', accentClass: 'accent--saving' },
current: { label: 'Current Accounts', icon: 'building', color: '#00796b', accentClass: 'accent--current' },
savings: {
label: 'Savings Accounts',
icon: 'piggy-bank',
accentClass: 'accent--saving',
typeLabel: 'SB',
typeBadgeClass: 'dtype--sb'
},
current: {
label: 'Current Accounts',
icon: 'building',
accentClass: 'accent--current',
typeLabel: 'CA',
typeBadgeClass: 'dtype--ca'
},
'fixed-deposits': {
label: 'Fixed Deposit Accounts',
icon: 'lock',
color: '#6a1b9a',
accentClass: 'accent--fd'
accentClass: 'accent--fd',
typeLabel: 'FD',
typeBadgeClass: 'dtype--fd'
},
'recurring-deposits': {
label: 'Recurring Deposit Accounts',
icon: 'sync',
color: '#e65100',
accentClass: 'accent--rd'
accentClass: 'accent--rd',
typeLabel: 'RD',
typeBadgeClass: 'dtype--rd'
}
};
@@ -141,6 +153,22 @@ export class AccountsListComponent implements OnInit, OnDestroy {
this.pageIndex = 0;
this.loadAccounts();
});
merge(this.fromDateControl.valueChanges, this.toDateControl.valueChanges)
.pipe(debounceTime(400), takeUntil(this.destroy$))
.subscribe(() => {
const from = this.fromDateControl.value;
const to = this.toDateControl.value;
if ((from && to) || (!from && !to)) {
this.pageIndex = 0;
this.loadAccounts();
}
});
this.asOnDateControl.valueChanges.pipe(debounceTime(300), takeUntil(this.destroy$)).subscribe(() => {
this.pageIndex = 0;
this.loadAccounts();
});
}
ngOnDestroy() {
@@ -151,11 +179,21 @@ export class AccountsListComponent implements OnInit, OnDestroy {
loadAccounts() {
this.isLoading = true;
const search = (this.searchControl.value || '').trim();
const params = {
const fromVal = this.fromDateControl.value;
const toVal = this.toDateControl.value;
const bothDatesSet = !!(fromVal && toVal);
const params: import('../accounts.service').AccountListParams = {
offset: this.pageIndex * this.pageSize,
limit: this.pageSize,
accountNo: this.searchType === 'accountNo' ? search : undefined,
clientName: this.searchType === 'clientName' ? search : undefined
accountNo: this.searchType === 'accountNo' ? search || undefined : undefined,
clientName: this.searchType === 'clientName' ? search || undefined : undefined,
orderBy: 'id',
sortOrder: 'DESC',
status: 'active',
...(bothDatesSet && { fromDate: fromVal! }),
...(bothDatesSet && { toDate: toVal! }),
...(this.asOnDateControl.value && { asOnDate: this.asOnDateControl.value })
};
const obs$ =
@@ -171,20 +209,33 @@ export class AccountsListComponent implements OnInit, OnDestroy {
next: (res) => {
let items: any[] = Array.isArray(res) ? res : (res?.pageItems ?? []);
// Client-side deposit type filter (safety net until backend rebuild)
const depositTypeFilter = this.getDepositTypeId();
if (depositTypeFilter !== null) {
items = items.filter((a: any) => a?.depositType?.id === depositTypeFilter);
if (this.accountType === 'savings') {
items = items.filter((a: any) => a?.depositType?.id === 100);
}
// Client-side search fallback (for when backend search isn't rebuilt yet)
const searchTerm = (this.searchControl.value || '').trim().toLowerCase();
items = items.filter((a: any) => (a?.status?.id ?? a?.statusId) === 300);
const searchTerm = search.toLowerCase();
if (searchTerm) {
items = items.filter((a: any) =>
this.searchType === 'accountNo'
? (a?.accountNo || '').toLowerCase().includes(searchTerm)
: (a?.clientName || '').toLowerCase().includes(searchTerm)
);
}
if (bothDatesSet || this.asOnDateControl.value) {
const fromDate = bothDatesSet ? new Date(fromVal!) : null;
const toDate = bothDatesSet ? new Date(toVal! + 'T23:59:59') : null;
const asOnDate = this.asOnDateControl.value ? new Date(this.asOnDateControl.value + 'T23:59:59') : null;
items = items.filter((a: any) => {
if (this.searchType === 'accountNo') {
return (a?.accountNo || '').toLowerCase().includes(searchTerm);
}
return (a?.clientName || '').toLowerCase().includes(searchTerm);
const d = this.parseFinDate(a?.activationDate ?? a?.submittedOnDate);
if (!d) return true;
if (fromDate && d < fromDate) return false;
if (toDate && d > toDate) return false;
if (asOnDate && d > asOnDate) return false;
return true;
});
}
@@ -204,93 +255,74 @@ export class AccountsListComponent implements OnInit, OnDestroy {
this.loadAccounts();
}
onSortChange(sort: Sort) {
private parseFinDate(d: any): Date | null {
if (!d) return null;
if (Array.isArray(d) && d.length >= 3) return new Date(d[0], d[1] - 1, d[2]);
if (typeof d === 'string') return new Date(d);
return null;
}
readonly today = new Date().toISOString().split('T')[0];
get hasDateFilters(): boolean {
return !!((this.fromDateControl.value && this.toDateControl.value) || this.asOnDateControl.value);
}
clearDateFilters() {
this.fromDateControl.setValue(null, { emitEvent: false });
this.toDateControl.setValue(null, { emitEvent: false });
this.asOnDateControl.setValue(null, { emitEvent: false });
this.pageIndex = 0;
this.loadAccounts();
}
onSortChange(_sort: Sort) {
this.pageIndex = 0;
this.loadAccounts();
}
toggleSearchType() {
this.searchType = this.searchType === 'accountNo' ? 'clientName' : 'accountNo';
if (this.searchControl.value) {
this.loadAccounts();
}
if (this.searchControl.value) this.loadAccounts();
}
clearSearch() {
this.searchControl.setValue('');
}
getDepositTypeId(): number | null {
// Only filter on savings page — FD/RD endpoints already return correct type
return this.accountType === 'savings' ? 100 : null;
}
getDepositTypeBadgeClass(_account: any): string {
switch (this.accountType) {
case 'savings':
return 'dtype--sb';
case 'fixed-deposits':
return 'dtype--fd';
case 'recurring-deposits':
return 'dtype--rd';
default:
return 'dtype--ca';
}
}
getDepositTypeLabel(_account: any): string {
switch (this.accountType) {
case 'savings':
return 'SB';
case 'fixed-deposits':
return 'FD';
case 'recurring-deposits':
return 'RD';
default:
return 'CA';
}
}
viewAccount(account: any) {
const id = account.id;
switch (this.accountType) {
case 'savings':
this.router.navigate([
'/clients',
account.clientId,
'savings-accounts',
id,
'general'
]);
break;
case 'current':
this.router.navigate([
'/clients',
account.clientId,
'current-accounts',
id,
'general'
]);
break;
case 'fixed-deposits':
this.router.navigate([
'/clients',
account.clientId,
'fixed-deposits-accounts',
id,
'general'
]);
break;
case 'recurring-deposits':
this.router.navigate([
'/clients',
account.clientId,
'recurring-deposits-accounts',
id,
'general'
]);
break;
}
const routes: Record<AccountType, string[]> = {
savings: [
'/clients',
account.clientId,
'savings-accounts',
id,
'general'
],
current: [
'/clients',
account.clientId,
'current-accounts',
id,
'general'
],
'fixed-deposits': [
'/clients',
account.clientId,
'fixed-deposits-accounts',
id,
'general'
],
'recurring-deposits': [
'/clients',
account.clientId,
'recurring-deposits-accounts',
id,
'general'
]
};
this.router.navigate(routes[this.accountType]);
}
getBalance(account: any): number {
@@ -303,22 +335,15 @@ export class AccountsListComponent implements OnInit, OnDestroy {
getStatusClass(account: any): string {
const id = account?.status?.id ?? account?.statusId;
switch (id) {
case 100:
return 'status--submitted';
case 200:
return 'status--approved';
case 300:
return 'status--active';
case 400:
return 'status--withdrawn';
case 500:
return 'status--rejected';
case 600:
return 'status--closed';
default:
return 'status--default';
}
const map: Record<number, string> = {
100: 'status--submitted',
200: 'status--approved',
300: 'status--active',
400: 'status--withdrawn',
500: 'status--rejected',
600: 'status--closed'
};
return map[id] ?? 'status--default';
}
getStatusLabel(account: any): string {
+22
View File
@@ -17,6 +17,10 @@ export interface AccountListParams {
clientName?: string;
orderBy?: string;
sortOrder?: string;
status?: string;
fromDate?: string;
toDate?: string;
asOnDate?: string;
}
@Injectable({ providedIn: 'root' })
@@ -32,6 +36,10 @@ export class AccountsService {
if (params.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder);
if (params.status) httpParams = httpParams.set('status', params.status);
if (params.fromDate) httpParams = httpParams.set('fromDate', params.fromDate);
if (params.toDate) httpParams = httpParams.set('toDate', params.toDate);
if (params.asOnDate) httpParams = httpParams.set('asOnDate', params.asOnDate);
return this.http.get('/savingsaccounts', { params: httpParams });
}
@@ -41,6 +49,12 @@ export class AccountsService {
.set('limit', (params.limit ?? 20).toString());
if (params.accountNo) httpParams = httpParams.set('accountNo', params.accountNo);
if (params.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder);
if (params.status) httpParams = httpParams.set('status', params.status);
if (params.fromDate) httpParams = httpParams.set('fromDate', params.fromDate);
if (params.toDate) httpParams = httpParams.set('toDate', params.toDate);
if (params.asOnDate) httpParams = httpParams.set('asOnDate', params.asOnDate);
return this.http.get('/currentaccounts', { params: httpParams });
}
@@ -53,6 +67,10 @@ export class AccountsService {
if (params.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder);
if (params.status) httpParams = httpParams.set('status', params.status);
if (params.fromDate) httpParams = httpParams.set('fromDate', params.fromDate);
if (params.toDate) httpParams = httpParams.set('toDate', params.toDate);
if (params.asOnDate) httpParams = httpParams.set('asOnDate', params.asOnDate);
return this.http.get('/fixeddepositaccounts', { params: httpParams });
}
@@ -65,6 +83,10 @@ export class AccountsService {
if (params.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder);
if (params.status) httpParams = httpParams.set('status', params.status);
if (params.fromDate) httpParams = httpParams.set('fromDate', params.fromDate);
if (params.toDate) httpParams = httpParams.set('toDate', params.toDate);
if (params.asOnDate) httpParams = httpParams.set('asOnDate', params.asOnDate);
return this.http.get('/recurringdepositaccounts', { params: httpParams });
}
}