KYC management: stage-wise notices, pagination, RD base chart

This commit is contained in:
2026-10-20 17:23:42 +05:30
parent 3e5cc26e49
commit 357f666421
108 changed files with 15001 additions and 2721 deletions
@@ -90,6 +90,21 @@
<mifosx-long-text textValue="{{ element.productName }}" chars="35"></mifosx-long-text>
</td>
</ng-container>
<ng-container matColumnDef="Branch">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Branch' | translate }}</th>
<td mat-cell *matCellDef="let element">
@if (element.officeName) {
<span [class]="branchChipClass(element)" [title]="branchChipTitle(element)">
<i class="fa fa-map-marker"></i>{{ element.officeName }}
@if (isHomeBranch(element)) {
<span class="branch-chip__home">Home</span>
}
</span>
} @else {
—
}
</td>
</ng-container>
<ng-container matColumnDef="Last Active">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Last Active' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.lastActiveTransactionDate | dateFormat }}</td>
@@ -203,6 +218,21 @@
<mifosx-long-text textValue="{{ element.productName }}" chars="35"></mifosx-long-text>
</td>
</ng-container>
<ng-container matColumnDef="Branch">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Branch' | translate }}</th>
<td mat-cell *matCellDef="let element">
@if (element.officeName) {
<span [class]="branchChipClass(element)" [title]="branchChipTitle(element)">
<i class="fa fa-map-marker"></i>{{ element.officeName }}
@if (isHomeBranch(element)) {
<span class="branch-chip__home">Home</span>
}
</span>
} @else {
—
}
</td>
</ng-container>
<ng-container matColumnDef="Closed Date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Closed Date' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.timeline.closedOnDate | dateFormat }}</td>
@@ -262,6 +292,21 @@
<mifosx-long-text textValue="{{ element.productName }}" chars="35"></mifosx-long-text>
</td>
</ng-container>
<ng-container matColumnDef="Branch">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Branch' | translate }}</th>
<td mat-cell *matCellDef="let element">
@if (element.officeName) {
<span [class]="branchChipClass(element)" [title]="branchChipTitle(element)">
<i class="fa fa-map-marker"></i>{{ element.officeName }}
@if (isHomeBranch(element)) {
<span class="branch-chip__home">Home</span>
}
</span>
} @else {
—
}
</td>
</ng-container>
<ng-container matColumnDef="Last Active">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Last Active' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.lastActiveTransactionDate | dateFormat }}</td>
@@ -452,6 +497,21 @@
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fixed Deposit Product' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.productName }}</td>
</ng-container>
<ng-container matColumnDef="Branch">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Branch' | translate }}</th>
<td mat-cell *matCellDef="let element">
@if (element.officeName) {
<span [class]="branchChipClass(element)" [title]="branchChipTitle(element)">
<i class="fa fa-map-marker"></i>{{ element.officeName }}
@if (isHomeBranch(element)) {
<span class="branch-chip__home">Home</span>
}
</span>
} @else {
—
}
</td>
</ng-container>
<ng-container matColumnDef="Last Active">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Last Active' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.lastActiveTransactionDate | dateFormat }}</td>
@@ -551,6 +611,21 @@
<mifosx-long-text textValue="{{ element.productName }}" chars="35"></mifosx-long-text>
</td>
</ng-container>
<ng-container matColumnDef="Branch">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Branch' | translate }}</th>
<td mat-cell *matCellDef="let element">
@if (element.officeName) {
<span [class]="branchChipClass(element)" [title]="branchChipTitle(element)">
<i class="fa fa-map-marker"></i>{{ element.officeName }}
@if (isHomeBranch(element)) {
<span class="branch-chip__home">Home</span>
}
</span>
} @else {
—
}
</td>
</ng-container>
<ng-container matColumnDef="Closed Date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Closed Date' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.timeline.closedOnDate | dateFormat }}</td>
@@ -611,10 +686,27 @@
<mifosx-long-text textValue="{{ element.productName }}" chars="35"></mifosx-long-text>
</td>
</ng-container>
<ng-container matColumnDef="Branch">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Branch' | translate }}</th>
<td mat-cell *matCellDef="let element">
@if (element.officeName) {
<span [class]="branchChipClass(element)" [title]="branchChipTitle(element)">
<i class="fa fa-map-marker"></i>{{ element.officeName }}
@if (isHomeBranch(element)) {
<span class="branch-chip__home">Home</span>
}
</span>
} @else {
—
}
</td>
</ng-container>
<ng-container matColumnDef="Status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let element">
<span class="rd-status" [class.rd-status--ended]="element.timeline?.closedOnDate">{{ rdStatusLabel(element) }}</span>
<span class="rd-status" [class.rd-status--ended]="element.timeline?.closedOnDate">{{
rdStatusLabel(element)
}}</span>
@if (element.timeline?.closedOnDate) {
<div class="rd-status__date">on {{ element.timeline.closedOnDate | dateFormat }}</div>
}
@@ -440,3 +440,66 @@ table[mat-table] {
font-size: 11px;
color: #64748b;
}
/*
* Branch an account is held at, as a pill coloured by where the signed-in user works: green for their own branch, red for
* any other. When the customer banks at several other branches the reds run from light to dark, one shade per branch.
*/
.branch-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
border-radius: 999px;
border: 1px solid transparent;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
.fa {
font-size: 12px;
}
&--mine {
background: #dcfce7;
color: #166534;
border-color: #4ade80;
}
&--other-0 {
background: #fee2e2;
color: #991b1b;
border-color: #fca5a5;
}
&--other-1 {
background: #fca5a5;
color: #7f1d1d;
border-color: #f87171;
}
&--other-2 {
background: #dc2626;
color: #fff;
border-color: #b91c1c;
}
&--other-3 {
background: #991b1b;
color: #fff;
border-color: #7f1d1d;
}
}
.branch-chip__home {
padding: 0 6px;
border-radius: 8px;
/* fixed colours: the chip text turns white on the dark reds, which would vanish against this light tag */
background: rgb(255 255 255 / 85%);
color: #1f2937;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
@@ -39,6 +39,7 @@ import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { CurrencyPipe } from '@angular/common';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { AuthenticationService } from '../../../core/authentication/authentication.service';
import { ReportsService } from 'app/reports/reports.service';
import { SettingsService } from 'app/settings/settings.service';
import { Subject } from 'rxjs';
@@ -151,6 +152,7 @@ export class GeneralTabComponent implements OnDestroy {
private router = inject(Router);
private reportsService = inject(ReportsService);
private settingsService = inject(SettingsService);
private authenticationService = inject(AuthenticationService);
/** Open Loan Accounts Columns */
openLoansColumns: string[] = [
@@ -177,6 +179,7 @@ export class GeneralTabComponent implements OnDestroy {
openSavingsColumns: string[] = [
'Account No',
'Saving Account',
'Branch',
'Last Active',
'Balance',
'Actions'
@@ -185,12 +188,14 @@ export class GeneralTabComponent implements OnDestroy {
closedSavingsColumns: string[] = [
'Account No',
'Saving Account',
'Branch',
'Closed Date'
];
/** Closed Recurring Deposit Accounts Columns - with how each one ended, for tracking. */
recurringColumns: string[] = [
'Account No',
'Saving Account',
'Branch',
'Status',
'Last Active',
'Balance',
@@ -336,12 +341,76 @@ export class GeneralTabComponent implements OnDestroy {
/** Client Id */
clientid: any;
/** The customer's own (home) branch, to tell it from branches the customer banks at elsewhere. */
clientOfficeId: number | null = null;
/** How many red shades the branch chips have (general-tab.component.scss, .branch-chip--other-0 ... -3), light to dark. */
private static readonly OTHER_BRANCH_SHADES = 4;
/** The branch the signed-in user works at: accounts held here read green, every other branch red. */
private get myOfficeId(): number | null {
const officeId = this.authenticationService.getCredentials()?.officeId;
return officeId == null ? null : Number(officeId);
}
/**
* The other branches this customer banks at, lightest to darkest. Ordered by branch id so the shades stay put between
* visits; computed over every deposit account on the page so one branch has the same shade in every table.
*/
private otherBranchIds(): number[] {
const mine = this.myOfficeId;
const ids = new Set<number>();
for (const account of [
...this.savingAccounts,
...this.currentAccounts
]) {
const officeId = Number(account?.officeId);
if (Number.isFinite(officeId) && officeId !== mine) {
ids.add(officeId);
}
}
return [...ids].sort((a, b) => a - b);
}
/** Green for an account at the signed-in user's branch; red for any other branch, a different shade for each. */
branchChipClass(account: any): string {
const officeId = Number(account?.officeId);
if (officeId === this.myOfficeId) {
return 'branch-chip branch-chip--mine';
}
const others = this.otherBranchIds();
const rank = Math.max(others.indexOf(officeId), 0);
const last = GeneralTabComponent.OTHER_BRANCH_SHADES - 1;
// one other branch gets a mid red, several are spread from the lightest to the darkest
const shade = others.length <= 1 ? 1 : Math.round((rank * last) / (others.length - 1));
return `branch-chip branch-chip--other-${shade}`;
}
branchChipTitle(account: any): string {
const parts = [account?.officeName];
if (Number(account?.officeId) === this.myOfficeId) {
parts.push('your branch');
}
if (this.isHomeBranch(account)) {
parts.push('customer home branch');
}
return parts.join(' - ');
}
isHomeBranch(account: any): boolean {
return this.clientOfficeId != null && Number(account?.officeId) === this.clientOfficeId;
}
/**
* @param {ActivatedRoute} route Activated Route
* @param {ClientsService} clientService Clients Service
* @param {Router} router Router
*/
constructor() {
// The customer's record is resolved on the parent route, not on this tab's own route data.
this.route.parent?.data.subscribe((data: any) => {
this.clientOfficeId = data?.clientViewData?.officeId ?? null;
});
this.route.data.subscribe(
(data: { clientAccountsData: any; clientChargesData: any; clientSummary: any; clientCollateralData: any }) => {
this.clientAccountData = data.clientAccountsData;