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:
co-authored by
Claude Sonnet 4.6
parent
45d4397b0e
commit
846b2484d7
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user