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"> <div class="acl-page">
<!-- Header --> <!-- Header -->
<div class="acl-header" [ngClass]="config.accentClass"> <div class="acl-header" [ngClass]="config.accentClass">
<div class="acl-header__left"> <div class="acl-header__icon">
<div class="acl-header__icon-wrap"> <fa-icon [icon]="config.icon"></fa-icon>
<fa-icon [icon]="config.icon"></fa-icon> </div>
</div> <div class="acl-header__text">
<div> <h2 class="acl-header__title">{{ config.label }}</h2>
<h2 class="acl-header__title">{{ config.label }}</h2> <span class="acl-header__count">
<span class="acl-header__subtitle"> @if (isLoading) {
@if (!isLoading) { Loading...
{{ totalRecords }} total records } @else {
} @else { {{ totalRecords }} active accounts
Loading... }
} </span>
</span>
</div>
</div> </div>
</div> </div>
<!-- Toolbar --> <!-- Search Bar -->
<div class="acl-toolbar"> <div class="acl-search-wrap">
<div class="acl-search-field"> <div class="acl-search">
<fa-icon icon="search" class="acl-search-field__icon"></fa-icon> <fa-icon icon="search" class="acl-search__icon"></fa-icon>
<input <input
[formControl]="searchControl" [formControl]="searchControl"
class="acl-search-field__input" class="acl-search__input"
[placeholder]="searchType === 'accountNo' ? 'Search by account number...' : 'Search by client name...'" [placeholder]="searchType === 'accountNo' ? 'Search by account number…' : 'Search by client name…'"
/> />
@if (searchControl.value) { @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> <fa-icon icon="times"></fa-icon>
</button> </button>
} }
</div> </div>
<button <button
class="acl-toggle-btn" class="acl-btn acl-btn--toggle"
(click)="toggleSearchType()" (click)="toggleSearchType()"
[matTooltip]="'Switch to ' + (searchType === 'accountNo' ? 'client name' : 'account number') + ' search'" [matTooltip]="'Switch to ' + (searchType === 'accountNo' ? 'client name' : 'account number') + ' search'"
> >
<fa-icon icon="exchange-alt"></fa-icon> <fa-icon icon="exchange-alt"></fa-icon>
{{ searchType === 'accountNo' ? 'Acct No' : 'Client' }} {{ searchType === 'accountNo' ? 'Acct No' : 'Client' }}
</button> </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> <fa-icon icon="sync-alt"></fa-icon>
</button> </button>
</div> </div>
<!-- Table Card --> <!-- Date Filters -->
<div class="acl-table-card"> <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) { @if (isLoading) {
<div class="acl-loading"> <div class="acl-state acl-state--loading">
<mat-spinner diameter="36"></mat-spinner> <mat-spinner diameter="32"></mat-spinner>
<span>Loading accounts...</span> <span>Loading accounts…</span>
</div> </div>
} }
@if (!isLoading && dataSource.data.length === 0) { @if (!isLoading && dataSource.data.length === 0) {
<div class="acl-empty"> <div class="acl-state acl-state--empty">
<fa-icon icon="inbox" size="2x" class="acl-empty__icon"></fa-icon> <div class="acl-state__icon">
<p>No accounts found</p> <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> </div>
} }
@if (!isLoading && dataSource.data.length > 0) { @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"> <table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)" class="acl-table">
<!-- Account No -->
<ng-container matColumnDef="accountNo"> <ng-container matColumnDef="accountNo">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Account No</th> <th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Account No</th>
<td mat-cell *matCellDef="let row" class="acl-td"> <td mat-cell *matCellDef="let row" class="acl-td">
@@ -81,33 +115,30 @@
</td> </td>
</ng-container> </ng-container>
<!-- Type -->
<ng-container matColumnDef="depositType"> <ng-container matColumnDef="depositType">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--center">Type</th> <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"> <td mat-cell *matCellDef="let row" class="acl-td acl-td--center">
<span class="acl-dtype-badge" [ngClass]="getDepositTypeBadgeClass(row)"> <span class="acl-type-badge" [ngClass]="config.typeBadgeClass">{{ config.typeLabel }}</span>
{{ getDepositTypeLabel(row) }}
</span>
</td> </td>
</ng-container> </ng-container>
<!-- Client -->
<ng-container matColumnDef="clientName"> <ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Client</th> <th mat-header-cell *matHeaderCellDef mat-sort-header class="acl-th">Client</th>
<td mat-cell *matCellDef="let row" class="acl-td"> <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> </td>
</ng-container> </ng-container>
<!-- Product -->
<ng-container matColumnDef="productName"> <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"> <td mat-cell *matCellDef="let row" class="acl-td">
<span class="acl-product">{{ row.savingsProductName || row.depositProductName || row.productName }}</span> <span class="acl-product">{{ row.savingsProductName || row.depositProductName || row.productName }}</span>
</td> </td>
</ng-container> </ng-container>
<!-- Balance -->
<ng-container matColumnDef="balance"> <ng-container matColumnDef="balance">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--right">Balance</th> <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"> <td mat-cell *matCellDef="let row" class="acl-td acl-td--right">
@@ -115,37 +146,24 @@
</td> </td>
</ng-container> </ng-container>
<!-- Status -->
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--center">Status</th> <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"> <td mat-cell *matCellDef="let row" class="acl-td acl-td--center">
<span class="acl-status-badge" [ngClass]="getStatusClass(row)"> <span class="acl-status" [ngClass]="getStatusClass(row)">{{ getStatusLabel(row) }}</span>
{{ getStatusLabel(row) }}
</span>
</td> </td>
</ng-container> </ng-container>
<!-- Actions -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="acl-th acl-th--center">Actions</th> <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"> <td mat-cell *matCellDef="let row" class="acl-td acl-td--center">
<button <button class="acl-view-btn" (click)="$event.stopPropagation(); viewAccount(row)" matTooltip="View">
class="acl-view-btn"
(click)="$event.stopPropagation(); viewAccount(row)"
matTooltip="View Account"
>
<fa-icon icon="eye"></fa-icon> <fa-icon icon="eye"></fa-icon>
</button> </button>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns" class="acl-header-row"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns" class="acl-header-row"></tr>
<tr <tr mat-row *matRowDef="let row; columns: displayedColumns" class="acl-row" (click)="viewAccount(row)"></tr>
mat-row
*matRowDef="let row; columns: displayedColumns"
class="acl-data-row"
(click)="viewAccount(row)"
></tr>
</table> </table>
</div> </div>
@@ -6,18 +6,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
// ─── Tokens ─────────────────────────────────────────────────────────────────── $t: 0.18s ease;
$radius-sm: 8px; $r-sm: 10px;
$radius-md: 12px; $r-md: 14px;
$radius-lg: 16px; $r-lg: 18px;
$shadow-card: $shadow:
0 1px 3px rgb(0 0 0 / 6%), 0 1px 3px rgb(0 0 0 / 5%),
0 4px 16px rgb(0 0 0 / 6%); 0 4px 20px rgb(0 0 0 / 6%);
$transition: 0.15s ease; $shadow-sm: 0 1px 3px rgb(0 0 0 / 8%);
// ─── Page ───────────────────────────────────────────────────────────────────── // ─── Page ─────────────────────────────────────────────────────────────────────
.acl-page { .acl-page {
padding: 1.25rem 1.5rem; padding: 1.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
@@ -29,114 +29,106 @@ $transition: 0.15s ease;
.acl-header { .acl-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 1rem;
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
border-radius: $radius-lg;
background: #fff; background: #fff;
box-shadow: $shadow-card; border-radius: $r-lg;
box-shadow: $shadow;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&::before { &::after {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0 auto 0 0; inset: 0 auto 0 0;
width: 5px; width: 4px;
background: var(--accent-color, #1976d2); background: var(--ac, #1976d2);
border-radius: $radius-lg 0 0 $radius-lg; border-radius: $r-lg 0 0 $r-lg;
} }
&__left { &__icon {
display: flex;
align-items: center;
gap: 1rem;
}
&__icon-wrap {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: $radius-md; border-radius: $r-md;
background: var(--accent-bg, #e3f2fd); background: var(--ac-bg, #e3f2fd);
color: var(--ac, #1976d2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.35rem; font-size: 1.3rem;
color: var(--accent-color, #1976d2);
flex-shrink: 0; flex-shrink: 0;
} }
&__title { &__title {
margin: 0; margin: 0 0 2px;
font-size: 1.1rem; font-size: 1.05rem;
font-weight: 700; font-weight: 700;
color: #1a2332; color: #0f172a;
letter-spacing: -0.01em; letter-spacing: -0.01em;
line-height: 1.3;
} }
&__subtitle { &__count {
font-size: 0.73rem; font-size: 0.72rem;
color: #94a3b8; color: #94a3b8;
margin-top: 3px;
display: block;
font-weight: 500; font-weight: 500;
} }
// ── Accent variants ──────────────────────────────────────────────────────── // accent variants
&.accent--saving { &.accent--saving {
--accent-color: #1565c0; --ac: #1565c0;
--accent-bg: #dbeafe; --ac-bg: #dbeafe;
} }
&.accent--current { &.accent--current {
--accent-color: #065f46; --ac: #065f46;
--accent-bg: #d1fae5; --ac-bg: #d1fae5;
} }
&.accent--fd { &.accent--fd {
--accent-color: #6b21a8; --ac: #6b21a8;
--accent-bg: #f3e8ff; --ac-bg: #f3e8ff;
} }
&.accent--rd { &.accent--rd {
--accent-color: #c2410c; --ac: #c2410c;
--accent-bg: #ffedd5; --ac-bg: #ffedd5;
} }
} }
// ─── Toolbar (Search + Actions) ─────────────────────────────────────────────── // ─── Search Bar ───────────────────────────────────────────────────────────────
.acl-toolbar { .acl-search-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.6rem;
padding: 0.75rem 1rem;
background: #fff; background: #fff;
border-radius: $radius-md; border-radius: $r-md;
box-shadow: $shadow-card; box-shadow: $shadow;
padding: 0.6rem 0.75rem;
} }
.acl-search-field { .acl-search {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; gap: 0.6rem;
background: #f8fafc; background: #f8fafc;
border: 1.5px solid #e2e8f0; border: 1.5px solid #e2e8f0;
border-radius: $radius-sm; border-radius: $r-sm;
padding: 0.45rem 0.9rem; padding: 0.42rem 0.85rem;
transition: transition:
border-color $transition, border-color $t,
box-shadow $transition; box-shadow $t,
background $t;
&:focus-within { &:focus-within {
border-color: #3b82f6; 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; background: #fff;
} }
&__icon { &__icon {
color: #94a3b8; color: #94a3b8;
font-size: 0.8rem; font-size: 0.78rem;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -159,9 +151,9 @@ $transition: 0.15s ease;
color: #cbd5e1; color: #cbd5e1;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
font-size: 0.75rem; font-size: 0.72rem;
transition: color $t;
line-height: 1; line-height: 1;
transition: color $transition;
&:hover { &:hover {
color: #64748b; color: #64748b;
@@ -169,183 +161,343 @@ $transition: 0.15s ease;
} }
} }
.acl-toggle-btn, .acl-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;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; 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: transition:
background $transition, background $t,
color $transition, color $t,
border-color $transition; border-color $t;
&:hover { &:hover {
background: #eff6ff; background: #eff6ff;
color: #1d4ed8; color: #1d4ed8;
border-color: #bfdbfe; border-color: #bfdbfe;
} }
&--icon {
width: 38px;
height: 38px;
padding: 0;
justify-content: center;
flex-shrink: 0;
}
} }
.acl-refresh-btn { // ─── Date Filters ─────────────────────────────────────────────────────────────
width: 38px; .acl-filters {
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 {
display: flex; display: flex;
flex-direction: column;
align-items: center; 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; gap: 0.5rem;
padding: 4rem; background: #fff;
color: #cbd5e1; 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 { &__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 { &__title {
font-size: 0.88rem;
font-weight: 500;
color: #94a3b8;
margin: 0; margin: 0;
font-size: 0.92rem;
font-weight: 600;
color: #475569;
} }
}
.acl-table-wrapper { &__sub {
overflow-x: auto; margin: 0;
font-size: 0.78rem;
color: #94a3b8;
}
} }
// ─── Table ──────────────────────────────────────────────────────────────────── // ─── Table ────────────────────────────────────────────────────────────────────
.acl-table-wrap {
overflow-x: auto;
}
.acl-table { .acl-table {
width: 100%; width: 100%;
.acl-header-row { .acl-header-row {
background: #f8fafc; background: #f8fafc;
border-bottom: 2px solid #e2e8f0; border-bottom: 1.5px solid #f1f5f9;
} }
.acl-th { .acl-th {
font-size: 0.65rem; font-size: 0.63rem;
font-weight: 700; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.07em; letter-spacing: 0.08em;
color: #64748b; color: #94a3b8;
padding: 0.75rem 1rem; padding: 0.85rem 1rem;
white-space: nowrap; white-space: nowrap;
border-bottom: none;
&--right {
text-align: right !important;
}
&--center { &--center {
text-align: center !important; text-align: center !important;
} }
&--right {
text-align: right !important;
}
} }
.acl-data-row { .acl-row {
cursor: pointer; cursor: pointer;
border-bottom: 1px solid #f8fafc; border-bottom: 1px solid #f8fafc;
transition: background $transition; transition: background $t;
&:hover {
background: #f0f7ff !important;
.acl-acct-no {
color: #1d4ed8;
background: #dbeafe;
}
}
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
&:hover {
background: #fafbff !important;
.acl-acct-no {
background: #dbeafe;
color: #1d4ed8;
}
}
} }
.acl-td { .acl-td {
padding: 0.7rem 1rem; padding: 0.75rem 1rem;
font-size: 0.82rem; font-size: 0.82rem;
vertical-align: middle; vertical-align: middle;
border-bottom: none;
&--right {
text-align: right !important;
}
&--center { &--center {
text-align: center !important; text-align: center !important;
} }
&--right {
text-align: right !important;
}
} }
} }
// ─── Cell Atoms ─────────────────────────────────────────────────────────────── // ─── Cell Atoms ───────────────────────────────────────────────────────────────
.acl-acct-no { .acl-acct-no {
font-family: 'Courier New', monospace; font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.77rem; font-size: 0.76rem;
font-weight: 700; font-weight: 700;
color: #1565c0; color: #1d4ed8;
background: #eff6ff; background: #eff6ff;
padding: 3px 10px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
white-space: nowrap; white-space: nowrap;
transition: transition:
background $transition, background $t,
color $transition; color $t;
letter-spacing: 0.3px;
} }
.acl-client-name { .acl-client {
font-weight: 600; display: flex;
color: #1e293b; align-items: center;
font-size: 0.82rem; 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 { .acl-product {
font-size: 0.75rem; font-size: 0.74rem;
color: #64748b; color: #64748b;
background: #f1f5f9; background: #f1f5f9;
padding: 3px 9px; border: 1px solid #e2e8f0;
border-radius: $radius-sm; padding: 3px 10px;
border-radius: $r-sm;
display: inline-block; display: inline-block;
max-width: 160px; max-width: 160px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
border: 1px solid #e2e8f0;
} }
.acl-balance { .acl-balance {
@@ -356,16 +508,16 @@ $transition: 0.15s ease;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
// ─── Deposit Type Badge ─────────────────────────────────────────────────────── // ─── Type Badge ───────────────────────────────────────────────────────────────
.acl-dtype-badge { .acl-type-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.62rem; font-size: 0.6rem;
font-weight: 800; font-weight: 900;
padding: 3px 10px; padding: 3px 10px;
border-radius: 20px; border-radius: 20px;
letter-spacing: 0.6px; letter-spacing: 0.8px;
text-transform: uppercase; text-transform: uppercase;
&.dtype--sb { &.dtype--sb {
@@ -394,14 +546,14 @@ $transition: 0.15s ease;
} }
// ─── Status Badge ───────────────────────────────────────────────────────────── // ─── Status Badge ─────────────────────────────────────────────────────────────
.acl-status-badge { .acl-status {
display: inline-block; display: inline-block;
font-size: 0.65rem; font-size: 0.63rem;
font-weight: 700; font-weight: 800;
padding: 3px 10px; padding: 3px 11px;
border-radius: 20px; border-radius: 20px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.6px;
white-space: nowrap; white-space: nowrap;
&.status--submitted { &.status--submitted {
@@ -447,11 +599,11 @@ $transition: 0.15s ease;
} }
} }
// ─── Action Button ──────────────────────────────────────────────────────────── // ─── View Button ──────────────────────────────────────────────────────────────
.acl-view-btn { .acl-view-btn {
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: $radius-sm; border-radius: $r-sm;
border: 1.5px solid #e2e8f0; border: 1.5px solid #e2e8f0;
background: #f8fafc; background: #f8fafc;
color: #64748b; color: #64748b;
@@ -461,10 +613,10 @@ $transition: 0.15s ease;
justify-content: center; justify-content: center;
font-size: 0.78rem; font-size: 0.78rem;
transition: transition:
background $transition, background $t,
color $transition, color $t,
border-color $transition, border-color $t,
box-shadow $transition; box-shadow $t;
&:hover { &:hover {
background: #eff6ff; background: #eff6ff;
@@ -479,4 +631,9 @@ $transition: 0.15s ease;
border-top: 1px solid #f1f5f9; border-top: 1px solid #f1f5f9;
background: #fafbfe; background: #fafbfe;
font-size: 0.8rem; font-size: 0.8rem;
::ng-deep .mat-mdc-paginator-container {
padding: 0.4rem 1rem;
min-height: 52px;
}
} }
@@ -24,14 +24,12 @@ import {
MatRowDef, MatRowDef,
MatRow MatRow
} from '@angular/material/table'; } from '@angular/material/table';
import { Subject } from 'rxjs'; import { Subject, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; 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 { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatCardModule } from '@angular/material/card';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatButtonModule } from '@angular/material/button';
import { FormatNumberPipe } from '../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { AccountsService } from '../accounts.service'; import { AccountsService } from '../accounts.service';
@@ -41,8 +39,9 @@ export type AccountType = 'savings' | 'current' | 'fixed-deposits' | 'recurring-
interface AccountConfig { interface AccountConfig {
label: string; label: string;
icon: string; icon: string;
color: string;
accentClass: string; accentClass: string;
typeLabel: string;
typeBadgeClass: string;
} }
@Component({ @Component({
@@ -68,11 +67,8 @@ interface AccountConfig {
MatRow, MatRow,
MatPaginator, MatPaginator,
NgClass, NgClass,
NgIf,
MatCardModule,
MatTooltipModule, MatTooltipModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatButtonModule,
FormatNumberPipe FormatNumberPipe
] ]
}) })
@@ -80,7 +76,6 @@ export class AccountsListComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private accountsService = inject(AccountsService); private accountsService = inject(AccountsService);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
accountType: AccountType = 'savings'; accountType: AccountType = 'savings';
@@ -102,24 +97,41 @@ export class AccountsListComponent implements OnInit, OnDestroy {
searchControl = new FormControl(''); searchControl = new FormControl('');
searchType: 'accountNo' | 'clientName' = 'accountNo'; searchType: 'accountNo' | 'clientName' = 'accountNo';
fromDateControl = new FormControl('');
toDateControl = new FormControl('');
asOnDateControl = new FormControl('');
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
readonly configs: Record<AccountType, AccountConfig> = { readonly configs: Record<AccountType, AccountConfig> = {
savings: { label: 'Savings Accounts', icon: 'piggy-bank', color: '#1976d2', accentClass: 'accent--saving' }, savings: {
current: { label: 'Current Accounts', icon: 'building', color: '#00796b', accentClass: 'accent--current' }, 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': { 'fixed-deposits': {
label: 'Fixed Deposit Accounts', label: 'Fixed Deposit Accounts',
icon: 'lock', icon: 'lock',
color: '#6a1b9a', accentClass: 'accent--fd',
accentClass: 'accent--fd' typeLabel: 'FD',
typeBadgeClass: 'dtype--fd'
}, },
'recurring-deposits': { 'recurring-deposits': {
label: 'Recurring Deposit Accounts', label: 'Recurring Deposit Accounts',
icon: 'sync', 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.pageIndex = 0;
this.loadAccounts(); 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() { ngOnDestroy() {
@@ -151,11 +179,21 @@ export class AccountsListComponent implements OnInit, OnDestroy {
loadAccounts() { loadAccounts() {
this.isLoading = true; this.isLoading = true;
const search = (this.searchControl.value || '').trim(); 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, offset: this.pageIndex * this.pageSize,
limit: this.pageSize, limit: this.pageSize,
accountNo: this.searchType === 'accountNo' ? search : undefined, accountNo: this.searchType === 'accountNo' ? search || undefined : undefined,
clientName: this.searchType === 'clientName' ? search : 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$ = const obs$ =
@@ -171,20 +209,33 @@ export class AccountsListComponent implements OnInit, OnDestroy {
next: (res) => { next: (res) => {
let items: any[] = Array.isArray(res) ? res : (res?.pageItems ?? []); let items: any[] = Array.isArray(res) ? res : (res?.pageItems ?? []);
// Client-side deposit type filter (safety net until backend rebuild) if (this.accountType === 'savings') {
const depositTypeFilter = this.getDepositTypeId(); items = items.filter((a: any) => a?.depositType?.id === 100);
if (depositTypeFilter !== null) {
items = items.filter((a: any) => a?.depositType?.id === depositTypeFilter);
} }
// Client-side search fallback (for when backend search isn't rebuilt yet) items = items.filter((a: any) => (a?.status?.id ?? a?.statusId) === 300);
const searchTerm = (this.searchControl.value || '').trim().toLowerCase();
const searchTerm = search.toLowerCase();
if (searchTerm) { 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) => { items = items.filter((a: any) => {
if (this.searchType === 'accountNo') { const d = this.parseFinDate(a?.activationDate ?? a?.submittedOnDate);
return (a?.accountNo || '').toLowerCase().includes(searchTerm); if (!d) return true;
} if (fromDate && d < fromDate) return false;
return (a?.clientName || '').toLowerCase().includes(searchTerm); 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(); 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.pageIndex = 0;
this.loadAccounts(); this.loadAccounts();
} }
toggleSearchType() { toggleSearchType() {
this.searchType = this.searchType === 'accountNo' ? 'clientName' : 'accountNo'; this.searchType = this.searchType === 'accountNo' ? 'clientName' : 'accountNo';
if (this.searchControl.value) { if (this.searchControl.value) this.loadAccounts();
this.loadAccounts();
}
} }
clearSearch() { clearSearch() {
this.searchControl.setValue(''); 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) { viewAccount(account: any) {
const id = account.id; const id = account.id;
switch (this.accountType) { const routes: Record<AccountType, string[]> = {
case 'savings': savings: [
this.router.navigate([ '/clients',
'/clients', account.clientId,
account.clientId, 'savings-accounts',
'savings-accounts', id,
id, 'general'
'general' ],
]); current: [
break; '/clients',
case 'current': account.clientId,
this.router.navigate([ 'current-accounts',
'/clients', id,
account.clientId, 'general'
'current-accounts', ],
id, 'fixed-deposits': [
'general' '/clients',
]); account.clientId,
break; 'fixed-deposits-accounts',
case 'fixed-deposits': id,
this.router.navigate([ 'general'
'/clients', ],
account.clientId, 'recurring-deposits': [
'fixed-deposits-accounts', '/clients',
id, account.clientId,
'general' 'recurring-deposits-accounts',
]); id,
break; 'general'
case 'recurring-deposits': ]
this.router.navigate([ };
'/clients', this.router.navigate(routes[this.accountType]);
account.clientId,
'recurring-deposits-accounts',
id,
'general'
]);
break;
}
} }
getBalance(account: any): number { getBalance(account: any): number {
@@ -303,22 +335,15 @@ export class AccountsListComponent implements OnInit, OnDestroy {
getStatusClass(account: any): string { getStatusClass(account: any): string {
const id = account?.status?.id ?? account?.statusId; const id = account?.status?.id ?? account?.statusId;
switch (id) { const map: Record<number, string> = {
case 100: 100: 'status--submitted',
return 'status--submitted'; 200: 'status--approved',
case 200: 300: 'status--active',
return 'status--approved'; 400: 'status--withdrawn',
case 300: 500: 'status--rejected',
return 'status--active'; 600: 'status--closed'
case 400: };
return 'status--withdrawn'; return map[id] ?? 'status--default';
case 500:
return 'status--rejected';
case 600:
return 'status--closed';
default:
return 'status--default';
}
} }
getStatusLabel(account: any): string { getStatusLabel(account: any): string {
+22
View File
@@ -17,6 +17,10 @@ export interface AccountListParams {
clientName?: string; clientName?: string;
orderBy?: string; orderBy?: string;
sortOrder?: string; sortOrder?: string;
status?: string;
fromDate?: string;
toDate?: string;
asOnDate?: string;
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -32,6 +36,10 @@ export class AccountsService {
if (params.clientName) httpParams = httpParams.set('clientName', params.clientName); if (params.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy); if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder); 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 }); return this.http.get('/savingsaccounts', { params: httpParams });
} }
@@ -41,6 +49,12 @@ export class AccountsService {
.set('limit', (params.limit ?? 20).toString()); .set('limit', (params.limit ?? 20).toString());
if (params.accountNo) httpParams = httpParams.set('accountNo', params.accountNo); if (params.accountNo) httpParams = httpParams.set('accountNo', params.accountNo);
if (params.clientName) httpParams = httpParams.set('clientName', params.clientName); 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 }); 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.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy); if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder); 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 }); 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.clientName) httpParams = httpParams.set('clientName', params.clientName);
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy); if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder); 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 }); return this.http.get('/recurringdepositaccounts', { params: httpParams });
} }
} }