Compare commits

...
4 Commits
Author SHA1 Message Date
niteshandClaude Sonnet 5 fcf6665772 fix(nav): restore Loan Origination and Investment menus dropped by merge
The "Merge dev into demo" commit silently dropped both top-level nav
entries (desktop and mobile variants) along with their entire menu
panels — Loan Origination's mat-menu and the whole Investment mega-menu
(~150 lines) — because dev never had these menus, so the merge picked
dev's shell.component.html wholesale for this file instead of combining
both sides' additions. The underlying feature modules and routes were
untouched, so this restores only the shell wiring, copied verbatim from
the pre-merge demo revision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 13:19:24 +05:30
nitesh f72abb0921 Merge remote-tracking branch 'origin/demo' into demo 2026-08-06 13:14:26 +05:30
niteshandClaude Sonnet 5 a1a50ab8a3 chore: update version stamp
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 13:10:52 +05:30
niteshandClaude Sonnet 5 858c277ae3 chore: update version stamp
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:17:49 +05:30
15 changed files with 536 additions and 145 deletions
@@ -1,3 +1,11 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="fm-dialog">
<div class="fm-dialog__header" style="display: flex; align-items: center; gap: 8px;">
<mat-icon class="fm-dialog__icon">people</mat-icon>
@@ -1,3 +1,11 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
+4 -1
View File
@@ -991,7 +991,10 @@ export class ClientsService {
* Resolves with HTTP 200 even when delivery fails — inspect `deliveryStatus` / `delivered` on the
* response rather than assuming success, since the log row is written either way.
*/
saveKycContactLog(clientId: string | number, data: { channel: string; notes: string; officerName?: string }): Observable<any> {
saveKycContactLog(
clientId: string | number,
data: { channel: string; notes: string; officerName?: string }
): Observable<any> {
return this.http.post(`/kyc/exceptions/${clientId}/contact-log`, data);
}
@@ -442,7 +442,9 @@ export class CreateClientComponent {
});
} else {
const { kycType, ...updatePayload } = this.clientKycStep.kycData;
this.clientsService.updateClientKyc(clientId, this.clientKycStep.kycId, updatePayload).subscribe({ error: () => {} });
this.clientsService
.updateClientKyc(clientId, this.clientKycStep.kycId, updatePayload)
.subscribe({ error: () => {} });
}
}
// Upload photo & signature
@@ -84,7 +84,7 @@
<mat-icon>phone_iphone</mat-icon>
<span>Mobile Number</span>
</div>
<div class="contact-verify-panel__exception-row" style="margin-bottom: 12px;">
<div class="contact-verify-panel__exception-row" style="margin-bottom: 12px">
<mat-checkbox
[checked]="oldMobileNotAccessible"
(change)="onOldMobileNotAccessibleToggle($event.checked)"
@@ -103,13 +103,27 @@
(stateChange)="onOldMobileOtpState($event)"
></mifosx-client-contact-otp-block>
} @else {
<div class="fir-upload-box" style="padding: 12px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #f8fafc;">
<div style="font-weight: 500; font-size: 13px; margin-bottom: 6px; color: #475569;">
<div
class="fir-upload-box"
style="padding: 12px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #f8fafc"
>
<div style="font-weight: 500; font-size: 13px; margin-bottom: 6px; color: #475569">
Upload Police Complaint / FIR Copy for Lost Mobile
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<input type="file" (change)="onFirFileSelected($event)" accept=".pdf,.jpg,.jpeg,.png" #firFileInput hidden />
<button mat-stroked-button type="button" (click)="firFileInput.click()" [disabled]="uploadingFir">
<div style="display: flex; align-items: center; gap: 12px">
<input
type="file"
(change)="onFirFileSelected($event)"
accept=".pdf,.jpg,.jpeg,.png"
#firFileInput
hidden
/>
<button
mat-stroked-button
type="button"
(click)="firFileInput.click()"
[disabled]="uploadingFir"
>
<mat-icon>upload_file</mat-icon>
{{ firFileName ? firFileName : 'Select FIR Document' }}
</button>
@@ -117,8 +131,18 @@
<mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
}
@if (firDocumentId && !uploadingFir) {
<span style="color: #16a34a; font-size: 12px; font-weight: 500; display: flex; align-items: center; gap: 4px;">
<mat-icon style="font-size: 16px; width: 16px; height: 16px;">check_circle</mat-icon> FIR Uploaded Successfully
<span
style="
color: #16a34a;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
"
>
<mat-icon style="font-size: 16px; width: 16px; height: 16px">check_circle</mat-icon> FIR
Uploaded Successfully
</span>
}
</div>
@@ -228,9 +228,7 @@ export class EditClientComponent {
get contactVerificationComplete(): boolean {
const changes = this.contactChanges;
if (changes.mobileChanged) {
const isOldSatisfied = this.oldMobileNotAccessible
? (this.firDocumentId != null)
: (this.oldMobileVerified);
const isOldSatisfied = this.oldMobileNotAccessible ? this.firDocumentId != null : this.oldMobileVerified;
if (!isOldSatisfied || !this.newMobileVerified) {
return false;
}
@@ -753,8 +751,10 @@ export class EditClientComponent {
const oldMobileNorm = normPhone(oldMobile);
// Mobile/Email are considered changed ONLY when the control has been edited (dirty) AND normalized values differ
const mobileChanged = !!oldMobileNorm && !!newMobileNorm && !!mobileControl?.dirty && (newMobileNorm !== oldMobileNorm);
const emailChanged = !!oldEmail && !!newEmail && !!emailControl?.dirty && (newEmail.toLowerCase() !== oldEmail.toLowerCase());
const mobileChanged =
!!oldMobileNorm && !!newMobileNorm && !!mobileControl?.dirty && newMobileNorm !== oldMobileNorm;
const emailChanged =
!!oldEmail && !!newEmail && !!emailControl?.dirty && newEmail.toLowerCase() !== oldEmail.toLowerCase();
return {
nameChanged: newName !== oldName,
+209
View File
@@ -363,6 +363,10 @@
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="loanOriginationMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="clipboard-check" class="ni-ico"></fa-icon>Loan Origination
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
@@ -375,6 +379,10 @@
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" (click)="investmentMenuOpen = !investmentMenuOpen; rbiMenuOpen = false; glMenuOpen = false" *mifosxHasPermission="'MENU_RBI'">
<fa-icon icon="chart-line" class="ni-ico"></fa-icon>Investment
<fa-icon [icon]="investmentMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu" *mifosxHasPermission="'MENU_DIGITAL'">
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
@@ -439,6 +447,10 @@
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="loanOriginationMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="clipboard-check" class="ni-ico"></fa-icon>Loan Origination
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu" *mifosxHasPermission="'MENU_ADVANCES'">
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
@@ -505,6 +517,15 @@
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a
class="ni"
tabindex="0"
(click)="investmentMenuOpen = !investmentMenuOpen; rbiMenuOpen = false; glMenuOpen = false"
*mifosxHasPermission="'MENU_RBI'"
>
<fa-icon icon="chart-line" class="ni-ico"></fa-icon>Investment
<fa-icon [icon]="investmentMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu" *mifosxHasPermission="'MENU_REPORTS'">
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
@@ -1271,6 +1292,39 @@
</div>
</mat-menu>
<mat-menu #loanOriginationMenu="matMenu" class="npa-mega-panel" [overlapTrigger]="false">
<div class="deposit-menu-grid npa-grid" (click)="$event.stopPropagation()">
<!-- Loan Origination -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Loan Origination</div>
<button mat-menu-item tabindex="0" routerLink="/loan-origination/pipeline">
<fa-icon icon="stream" class="menu-item-icon"></fa-icon> Application Pipeline
</button>
</div>
<!-- Reports -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Reports</div>
<button
mat-menu-item
tabindex="0"
routerLink="/loan-origination/pipeline"
[queryParams]="{ view: 'channel' }"
>
<fa-icon icon="chart-pie" class="menu-item-icon"></fa-icon> Channel-wise Report
</button>
<button mat-menu-item tabindex="0" routerLink="/loan-origination/pipeline" [queryParams]="{ view: 'bc' }">
<fa-icon icon="mobile-alt" class="menu-item-icon"></fa-icon> BC-wise Report
</button>
<button mat-menu-item tabindex="0" routerLink="/loan-origination/pipeline" [queryParams]="{ view: 'share' }">
<fa-icon icon="coins" class="menu-item-icon"></fa-icon> Share Member Report
</button>
<button mat-menu-item tabindex="0" routerLink="/loan-origination/pipeline" [queryParams]="{ view: 'target' }">
<fa-icon icon="chart-column" class="menu-item-icon"></fa-icon> Target vs Actual
</button>
</div>
</div>
</mat-menu>
<mat-menu #ckycMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
<button mat-menu-item routerLink="/ckyc" tabindex="0">
<fa-icon icon="id-card" class="menu-item-icon"></fa-icon> CKYC Management
@@ -1859,6 +1913,161 @@
</div>
}
<!-- Investment (Treasury) Mega Menu -->
@if (investmentMenuOpen) {
<div class="gl-backdrop" (click)="investmentMenuOpen = false"></div>
<div class="gl-mega-menu rbi-mega-menu">
<div class="gl-mega-menu-header">
<fa-icon icon="chart-line" class="gl-mega-menu-header-icon"></fa-icon>
<span>Investment</span>
</div>
<input
type="text"
class="gl-search"
placeholder="Search Investment..."
(input)="investmentSearch = $any($event.target).value"
/>
<div class="rbi-columns">
<!-- Dashboard -->
<div class="gl-group">
<div class="gl-group-title">Dashboard</div>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Investment Dashboard')"
routerLink="/investment/dashboard"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="chart-pie" class="gl-icon"></fa-icon> Portfolio Dashboard
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Due Date Diary for All Investment')"
routerLink="/investment/dashboard"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="calendar-check" class="gl-icon"></fa-icon> Due Date Diary for All Investment
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Bucket Wise Investment')"
routerLink="/investment/reports"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="layer-group" class="gl-icon"></fa-icon> Bucket Wise Investment
</a>
</div>
<!-- Entry -->
<div class="gl-group">
<div class="gl-group-title">Entry</div>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('FD Investment')"
routerLink="/investment/fd"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="piggy-bank" class="gl-icon"></fa-icon> FD Investment
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Govt Security / Non-SLR / Bond / Mutual Fund')"
routerLink="/investment/bonds"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="university" class="gl-icon"></fa-icon> Govt Security / Non-SLR / Bond / Mutual Fund
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('SLR and Non SLR Investment Report')"
routerLink="/investment/bonds"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="balance-scale" class="gl-icon"></fa-icon> SLR and Non SLR Investment Report
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Share Entry')"
routerLink="/investment/share"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="chart-line" class="gl-icon"></fa-icon> Share Entry
</a>
</div>
<!-- Masters -->
<div class="gl-group">
<div class="gl-group-title">Masters</div>
<a
class="gl-link"
[class.hidden]="
!matchesInvestmentSearch(
'Bank Party Script Broker SubType Control Parameter Daily Script LIEN Reason Mutual Fund Product SGL Bank Script Market Detail Masters'
)
"
routerLink="/investment/masters"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="cogs" class="gl-icon"></fa-icon> Bank/Party, Script, Broker &amp; Other Masters
</a>
</div>
<!-- Interest -->
<div class="gl-group">
<div class="gl-group-title">Interest</div>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('FD Interest Calculation')"
routerLink="/investment/interest"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="calculator" class="gl-icon"></fa-icon> FD Interest Calculation
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Govt Bond Interest Calculation')"
routerLink="/investment/interest"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="calculator" class="gl-icon"></fa-icon> Govt./Bond Int. Calculation
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Government Bond Int Register FD Interest Register FD Register')"
routerLink="/investment/interest"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="list-alt" class="gl-icon"></fa-icon> Bond Int / FD Int / FD Registers
</a>
</div>
<!-- Reports -->
<div class="gl-group">
<div class="gl-group-title">Reports</div>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Balance Ledger Maturity Investment Reports')"
routerLink="/investment/reports"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="file-invoice" class="gl-icon"></fa-icon> Balance / Ledger / Maturity Reports
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
routerLink="/investment/reports"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received &amp; Receivable Report
</a>
<a
class="gl-link"
[class.hidden]="!matchesInvestmentSearch('Investment Portfolio')"
routerLink="/investment/reports"
(click)="investmentMenuOpen = false"
>
<fa-icon icon="briefcase" class="gl-icon"></fa-icon> Investment Portfolio
</a>
</div>
</div>
</div>
}
<!-- Progress Bar -->
@if (progressBarMode !== 'none') {
<div>
@@ -771,27 +771,39 @@
<div class="km-card km-mt">
<!-- Tab Toggle Header -->
<div class="km-ch" style="border-bottom: 1px solid var(--km-g200); padding-bottom: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 12px;">
<div style="display: flex; gap: 8px; align-items: center;">
<div class="km-ci" style="background: #fef3c7; color: #d97706;"><fa-icon icon="exclamation-triangle"></fa-icon></div>
<div
class="km-ch"
style="
border-bottom: 1px solid var(--km-g200);
padding-bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
"
>
<div style="display: flex; gap: 8px; align-items: center">
<div class="km-ci" style="background: #fef3c7; color: #d97706">
<fa-icon icon="exclamation-triangle"></fa-icon>
</div>
<div class="km-ct">Exceptions Register</div>
</div>
<div class="km-inner-nav" style="width: 100%; border: none; margin: 10px 0 0 0; padding: 0; gap: 8px;">
<button
class="km-itab"
[class.active]="activeExceptionSubTab === 'pending'"
<div class="km-inner-nav" style="width: 100%; border: none; margin: 10px 0 0 0; padding: 0; gap: 8px">
<button
class="km-itab"
[class.active]="activeExceptionSubTab === 'pending'"
(click)="activeExceptionSubTab = 'pending'"
style="padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 13px;"
style="padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 13px"
>
<fa-icon icon="clock"></fa-icon>
Pending Exceptions ({{ pendingExceptions.length }})
</button>
<button
class="km-itab"
[class.active]="activeExceptionSubTab === 'resolved'"
<button
class="km-itab"
[class.active]="activeExceptionSubTab === 'resolved'"
(click)="activeExceptionSubTab = 'resolved'"
style="padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 13px;"
style="padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 13px"
>
<fa-icon icon="check-circle"></fa-icon>
Resolved Exceptions ({{ resolvedExceptions.length }})
@@ -802,10 +814,12 @@
<!-- PENDING EXCEPTIONS TAB -->
@if (activeExceptionSubTab === 'pending') {
@if (pendingExceptions.length === 0) {
<div class="km-empty" style="padding: 40px; text-align: center;">
<div class="km-empty" style="padding: 40px; text-align: center">
<div class="km-empty-i">🎉</div>
<div style="font-weight: 500; font-size: 16px; margin-top: 12px;">All exceptions resolved!</div>
<div class="km-empty-s" style="color: var(--km-g500); font-size: 13px; margin-top: 4px;">There are no active client KYC exceptions on record.</div>
<div style="font-weight: 500; font-size: 16px; margin-top: 12px">All exceptions resolved!</div>
<div class="km-empty-s" style="color: var(--km-g500); font-size: 13px; margin-top: 4px">
There are no active client KYC exceptions on record.
</div>
</div>
} @else {
<table class="km-tbl">
@@ -824,12 +838,18 @@
@for (c of pendingExceptions; track c.id) {
<tr>
<td>
<div class="km-an" [routerLink]="['/clients', c.id, 'kyc']" style="cursor: pointer; color: var(--km-blue); text-decoration: underline;">{{ c.name }}</div>
<div
class="km-an"
[routerLink]="['/clients', c.id, 'kyc']"
style="cursor: pointer; color: var(--km-blue); text-decoration: underline"
>
{{ c.name }}
</div>
<div class="km-ai">{{ c.account }}</div>
</td>
<td>{{ c.contact.branch }}</td>
<td>
<div style="font-weight: 500; color: #b45309;">{{ c.exceptionReason }}</div>
<div style="font-weight: 500; color: #b45309">{{ c.exceptionReason }}</div>
<div class="km-small">Opened: {{ c.exceptionDate }}</div>
</td>
<td>
@@ -838,20 +858,25 @@
<td>
@if (c.contactHistory && c.contactHistory.length > 0) {
<span class="km-del d-ok">Contacted</span>
<div class="km-small" style="margin-top: 2px;">Last: {{ c.contactHistory[c.contactHistory.length - 1].date }}</div>
<div class="km-small" style="margin-top: 2px">
Last: {{ c.contactHistory[c.contactHistory.length - 1].date }}
</div>
} @else {
<span class="km-del d-fa">Not Contacted</span>
}
</td>
<td>
@if (c.contactHistory && c.contactHistory.length > 0) {
<div class="contact-logs-list" style="max-height: 80px; overflow-y: auto; font-size: 11px;">
<div class="contact-logs-list" style="max-height: 80px; overflow-y: auto; font-size: 11px">
@for (log of c.contactHistory; track $index) {
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0;">
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }} <em>— {{ log.officer }}</em>
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0">
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }}
<em>— {{ log.officer }}</em>
@if (log.platformSent) {
@if (log.delivered) {
<span class="km-sent-tag km-sent-ok" [title]="log.deliveryMessage">Sent {{ log.destinationMasked }}</span>
<span class="km-sent-tag km-sent-ok" [title]="log.deliveryMessage"
>Sent {{ log.destinationMasked }}</span
>
} @else {
<span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span>
}
@@ -860,12 +885,16 @@
}
</div>
} @else {
<span style="color: var(--km-g500); font-size: 11px;">No logs recorded</span>
<span style="color: var(--km-g500); font-size: 11px">No logs recorded</span>
}
</td>
<td>
<div style="display: flex; gap: 8px;">
<button class="km-btn km-btn-ot" style="border-color: var(--km-blue); color: var(--km-blue);" [routerLink]="['/clients', c.id, 'kyc']">
<div style="display: flex; gap: 8px">
<button
class="km-btn km-btn-ot"
style="border-color: var(--km-blue); color: var(--km-blue)"
[routerLink]="['/clients', c.id, 'kyc']"
>
<fa-icon icon="user-check"></fa-icon> Do KYC
</button>
<button class="km-btn km-btn-ot" (click)="openContactDialog(c)">
@@ -883,10 +912,14 @@
<!-- RESOLVED EXCEPTIONS TAB -->
@if (activeExceptionSubTab === 'resolved') {
@if (resolvedExceptions.length === 0) {
<div class="km-empty" style="padding: 40px; text-align: center;">
<div class="km-empty-i" style="font-size: 32px; filter: grayscale(1);">📋</div>
<div style="font-weight: 500; font-size: 16px; margin-top: 12px; color: var(--km-g500);">No resolved exceptions</div>
<div class="km-empty-s" style="color: var(--km-g500); font-size: 13px; margin-top: 4px;">Resolved KYC exceptions will appear here once verification is completed.</div>
<div class="km-empty" style="padding: 40px; text-align: center">
<div class="km-empty-i" style="font-size: 32px; filter: grayscale(1)">📋</div>
<div style="font-weight: 500; font-size: 16px; margin-top: 12px; color: var(--km-g500)">
No resolved exceptions
</div>
<div class="km-empty-s" style="color: var(--km-g500); font-size: 13px; margin-top: 4px">
Resolved KYC exceptions will appear here once verification is completed.
</div>
</div>
} @else {
<table class="km-tbl">
@@ -904,12 +937,20 @@
@for (c of resolvedExceptions; track c.id) {
<tr>
<td>
<div class="km-an" [routerLink]="['/clients', c.id, 'kyc']" style="cursor: pointer; color: var(--km-blue); text-decoration: underline;">{{ c.name }}</div>
<div
class="km-an"
[routerLink]="['/clients', c.id, 'kyc']"
style="cursor: pointer; color: var(--km-blue); text-decoration: underline"
>
{{ c.name }}
</div>
<div class="km-ai">{{ c.account }}</div>
</td>
<td>{{ c.contact.branch }}</td>
<td>
<div style="font-weight: 500; color: var(--km-g500); text-decoration: line-through;">{{ c.exceptionReason }}</div>
<div style="font-weight: 500; color: var(--km-g500); text-decoration: line-through">
{{ c.exceptionReason }}
</div>
<div class="km-small">Resolved On: {{ c.exceptionResolvedOn }}</div>
</td>
<td>
@@ -917,13 +958,16 @@
</td>
<td>
@if (c.contactHistory && c.contactHistory.length > 0) {
<div class="contact-logs-list" style="max-height: 80px; overflow-y: auto; font-size: 11px;">
<div class="contact-logs-list" style="max-height: 80px; overflow-y: auto; font-size: 11px">
@for (log of c.contactHistory; track $index) {
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0;">
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }} <em>— {{ log.officer }}</em>
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0">
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }}
<em>— {{ log.officer }}</em>
@if (log.platformSent) {
@if (log.delivered) {
<span class="km-sent-tag km-sent-ok" [title]="log.deliveryMessage">Sent {{ log.destinationMasked }}</span>
<span class="km-sent-tag km-sent-ok" [title]="log.deliveryMessage"
>Sent {{ log.destinationMasked }}</span
>
} @else {
<span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span>
}
@@ -932,11 +976,23 @@
}
</div>
} @else {
<span style="color: var(--km-g500); font-size: 11px;">No logs recorded</span>
<span style="color: var(--km-g500); font-size: 11px">No logs recorded</span>
}
</td>
<td>
<span style="display: inline-flex; align-items: center; gap: 4px; background: #e6f4ea; color: #137333; padding: 4px 8px; border-radius: 4px; font-weight: 500; font-size: 12px;">
<span
style="
display: inline-flex;
align-items: center;
gap: 4px;
background: #e6f4ea;
color: #137333;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 12px;
"
>
<fa-icon icon="check-circle"></fa-icon> Exception Resolved
</span>
</td>
@@ -1421,7 +1477,9 @@
<label class="hv-label">Contact Channel<span class="hv-req">*</span></label>
<select class="hv-input" [(ngModel)]="contactChannel">
@for (ch of contactChannels; track ch.value) {
<option [value]="ch.value">{{ ch.label }}{{ ch.sends ? ' — sends a notice now' : ' — record only' }}</option>
<option [value]="ch.value">
{{ ch.label }}{{ ch.sends ? ' — sends a notice now' : ' — record only' }}
</option>
}
</select>
@if (selectedChannelSends) {
@@ -1430,14 +1488,12 @@
A KYC notice will be sent to the customer when you save.
</div>
} @else {
<div class="hv-hint">
Records an action you already took. Nothing is sent to the customer.
</div>
<div class="hv-hint">Records an action you already took. Nothing is sent to the customer.</div>
}
@if (selectedChannelUnavailable) {
<div class="hv-hint hv-hint-error">
This customer has no {{ selectedChannel?.requires === 'email' ? 'email address' : 'mobile number' }} on file —
choose a different channel.
This customer has no {{ selectedChannel?.requires === 'email' ? 'email address' : 'mobile number' }} on
file — choose a different channel.
</div>
}
</div>
@@ -1470,11 +1526,7 @@
<div class="hv-footer">
<button class="km-btn" [disabled]="contactSaving" (click)="closeContactDialog()">Cancel</button>
<button
class="km-btn km-btn-t"
[disabled]="contactSaveDisabled"
(click)="saveContactLog()"
>
<button class="km-btn km-btn-t" [disabled]="contactSaveDisabled" (click)="saveContactLog()">
{{ contactSaveLabel }}
</button>
</div>
@@ -1486,8 +1538,8 @@
@if (resolveDialogOpen && current) {
<div class="km-overlay" (click)="closeResolveDialog()">
<div class="km-modal hv-modal" (click)="$event.stopPropagation()">
<div class="km-mh" style="border-bottom: 2px solid var(--km-green);">
<div class="km-mh-av" style="background: var(--km-green);">{{ current.av }}</div>
<div class="km-mh" style="border-bottom: 2px solid var(--km-green)">
<div class="km-mh-av" style="background: var(--km-green)">{{ current.av }}</div>
<div>
<div class="km-mh-name">Resolve KYC Exception</div>
<div class="km-mh-meta">{{ current.name }} · {{ current.account }}</div>
@@ -1496,25 +1548,52 @@
</div>
<div class="km-mbody hv-body">
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; padding: 12px; margin-bottom: 16px; font-size: 13px; color: #166534;">
<div
style="
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 6px;
padding: 12px;
margin-bottom: 16px;
font-size: 13px;
color: #166534;
"
>
<strong>Pending Reason:</strong> {{ current.exceptionReason }}
@if (current.exceptionRemarks) {
<div style="margin-top: 4px; font-style: italic;">"{{ current.exceptionRemarks }}"</div>
<div style="margin-top: 4px; font-style: italic">"{{ current.exceptionRemarks }}"</div>
}
</div>
@if (showPanResolution) {
<div style="border: 1px solid var(--km-g300); border-radius: 6px; padding: 12px; margin-bottom: 16px; background: #fafafa;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<div
style="
border: 1px solid var(--km-g300);
border-radius: 6px;
padding: 12px;
margin-bottom: 16px;
background: #fafafa;
"
>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<input type="checkbox" id="resolvePanChk" [(ngModel)]="resolvePanActive" />
<label for="resolvePanChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800);">Resolve PAN Card Exception</label>
<label for="resolvePanChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800)"
>Resolve PAN Card Exception</label
>
</div>
@if (resolvePanActive) {
<div class="hv-row" style="margin-left: 20px;">
<div class="hv-row" style="margin-left: 20px">
<label class="hv-label">Verified PAN Number<span class="hv-req">*</span></label>
<input type="text" class="hv-input" [(ngModel)]="resolvePanNumber" placeholder="e.g. ABCDE1234F" maxlength="10" style="text-transform: uppercase;" />
<input
type="text"
class="hv-input"
[(ngModel)]="resolvePanNumber"
placeholder="e.g. ABCDE1234F"
maxlength="10"
style="text-transform: uppercase"
/>
@if (resolvePanNumber && resolvePanNumber.length !== 10) {
<div style="color: red; font-size: 11px; margin-top: 4px;">PAN must be exactly 10 characters.</div>
<div style="color: red; font-size: 11px; margin-top: 4px">PAN must be exactly 10 characters.</div>
}
</div>
}
@@ -1522,17 +1601,33 @@
}
@if (showAadhaarResolution) {
<div style="border: 1px solid var(--km-g300); border-radius: 6px; padding: 12px; margin-bottom: 16px; background: #fafafa;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<div
style="
border: 1px solid var(--km-g300);
border-radius: 6px;
padding: 12px;
margin-bottom: 16px;
background: #fafafa;
"
>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<input type="checkbox" id="resolveAadhChk" [(ngModel)]="resolveAadhaarActive" />
<label for="resolveAadhChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800);">Resolve Aadhaar Card Exception</label>
<label for="resolveAadhChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800)"
>Resolve Aadhaar Card Exception</label
>
</div>
@if (resolveAadhaarActive) {
<div class="hv-row" style="margin-left: 20px;">
<div class="hv-row" style="margin-left: 20px">
<label class="hv-label">Verified Aadhaar Number<span class="hv-req">*</span></label>
<input type="text" class="hv-input" [(ngModel)]="resolveAadhaarNumber" placeholder="e.g. 597855025299" maxlength="12" />
<input
type="text"
class="hv-input"
[(ngModel)]="resolveAadhaarNumber"
placeholder="e.g. 597855025299"
maxlength="12"
/>
@if (resolveAadhaarNumber && resolveAadhaarNumber.length !== 12) {
<div style="color: red; font-size: 11px; margin-top: 4px;">Aadhaar must be exactly 12 digits.</div>
<div style="color: red; font-size: 11px; margin-top: 4px">Aadhaar must be exactly 12 digits.</div>
}
</div>
}
@@ -1540,13 +1635,23 @@
}
@if (showGeneralResolution) {
<div style="border: 1px solid var(--km-g300); border-radius: 6px; padding: 12px; margin-bottom: 16px; background: #fafafa;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<div
style="
border: 1px solid var(--km-g300);
border-radius: 6px;
padding: 12px;
margin-bottom: 16px;
background: #fafafa;
"
>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<input type="checkbox" id="resolveGenChk" [(ngModel)]="resolveGeneralActive" />
<label for="resolveGenChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800);">Resolve General Exception</label>
<label for="resolveGenChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800)"
>Resolve General Exception</label
>
</div>
@if (resolveGeneralActive) {
<div style="margin-left: 20px;">
<div style="margin-left: 20px">
<div class="hv-row">
<label class="hv-label">Verification Document Type<span class="hv-req">*</span></label>
<select class="hv-input" [(ngModel)]="resolveGeneralDocumentType">
@@ -1555,7 +1660,12 @@
</div>
<div class="hv-row">
<label class="hv-label">Document Identification Number<span class="hv-req">*</span></label>
<input type="text" class="hv-input" [(ngModel)]="resolveGeneralDocumentNumber" placeholder="Enter verified document number" />
<input
type="text"
class="hv-input"
[(ngModel)]="resolveGeneralDocumentNumber"
placeholder="Enter verified document number"
/>
</div>
</div>
}
@@ -1575,11 +1685,7 @@
<div class="hv-footer">
<button class="km-btn" (click)="closeResolveDialog()">Cancel</button>
<button
class="km-btn km-btn-g"
[disabled]="!isResolveValid"
(click)="saveResolveKyc()"
>
<button class="km-btn km-btn-g" [disabled]="!isResolveValid" (click)="saveResolveKyc()">
Complete KYC &amp; Resolve Exception
</button>
</div>
@@ -1590,4 +1696,3 @@
<!-- TOAST -->
<div class="km-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
@@ -9,22 +9,16 @@
<h1 mat-dialog-title class="ced-title">
<span class="ced-title__icon">✉</span>
Approval Email
<div class="ced-status-row">
@if (data.emailSent) {
<span class="ced-chip ced-chip--sent">
<span class="ced-chip__dot"></span>Sent
</span>
<span class="ced-chip ced-chip--sent"> <span class="ced-chip__dot"></span>Sent </span>
} @else {
<span class="ced-chip ced-chip--notsent">
<span class="ced-chip__dot"></span>Not sent
</span>
<span class="ced-chip ced-chip--notsent"> <span class="ced-chip__dot"></span>Not sent </span>
}
@if (data.emailOpenedAt) {
<span class="ced-chip ced-chip--seen">
<fa-icon icon="eye" class="ced-chip__icon"></fa-icon>Seen
</span>
<span class="ced-chip ced-chip--seen"> <fa-icon icon="eye" class="ced-chip__icon"></fa-icon>Seen </span>
} @else {
<span class="ced-chip ced-chip--notseen">
<fa-icon icon="eye-slash" class="ced-chip__icon"></fa-icon>Not yet seen
@@ -34,16 +28,15 @@
</h1>
<div mat-dialog-content class="ced-content">
<!-- Classic Email Header (Gmail Style) -->
<div class="ced-gmail-header">
<h2 class="ced-gmail-subject">{{ data.emailSubject ?? '(No Subject)' }}</h2>
<div class="ced-gmail-meta">
<div class="ced-gmail-avatar">
<div class="ced-gmail-avatar-letter">C</div>
</div>
<div class="ced-gmail-meta-content">
<div class="ced-gmail-meta-top">
<span class="ced-gmail-sender-name">CBSNexora</span>
@@ -53,7 +46,7 @@
</div>
<div class="ced-gmail-meta-bottom">
<span class="ced-gmail-to">
to
to
@if (data.emailTo) {
{{ data.emailTo }}
} @else {
@@ -1,6 +1,9 @@
/**
* Copyright since 2025 Mifos Initiative
* MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
:host {
@@ -35,14 +38,14 @@
flex-direction: column;
margin-bottom: 30px;
padding: 0 4px;
font-family: 'Roboto', 'Inter', sans-serif; /* Gmail-like typography */
font-family: Roboto, Inter, sans-serif; /* Gmail-like typography */
}
.ced-gmail-subject {
font-size: 1.35rem;
font-weight: 400;
color: #202124;
margin: 0 0 24px 0;
margin: 0 0 24px;
line-height: 1.4;
}
@@ -153,13 +156,19 @@
&--sent {
background: #e8f5e9;
color: #1b5e20;
.ced-chip__dot { background: #43a047; }
.ced-chip__dot {
background: #43a047;
}
}
&--notsent {
background: #f5f5f5;
color: #757575;
.ced-chip__dot { background: #bdbdbd; }
.ced-chip__dot {
background: #bdbdbd;
}
}
&--seen {
@@ -176,7 +185,7 @@
/* ── Divider ────────────────────────────────────────────────────────────── */
.ced-divider {
border: none;
border-top: 1px solid #eeeeee;
border-top: 1px solid #eee;
margin: 12px 0;
}
@@ -197,13 +206,19 @@
width: 100%;
}
td, th {
td,
th {
padding: 6px 12px;
border: 1px solid #e0e0e0;
}
p { margin: 0 0 10px; }
ul { padding-left: 20px; }
p {
margin: 0 0 10px;
}
ul {
padding-left: 20px;
}
}
/* ── Empty state ────────────────────────────────────────────────────────── */
@@ -58,8 +58,12 @@ export class ClientEditEmailDialogComponent {
if (!isoDate) return '—';
const d = new Date(isoDate);
return d.toLocaleString('en-IN', {
day: '2-digit', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: true
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
}
}
@@ -127,18 +127,22 @@
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let edit">
<!-- ── Email status badges (compact, always visible) ── -->
<span class="pce-email-badge"
[class.pce-email-badge--sent]="edit.emailSent"
[class.pce-email-badge--nosent]="!edit.emailSent"
[title]="edit.emailSent ? 'Email sent on ' + formatDate(edit.emailSentAt) : 'No confirmation email sent'">
<span
class="pce-email-badge"
[class.pce-email-badge--sent]="edit.emailSent"
[class.pce-email-badge--nosent]="!edit.emailSent"
[title]="edit.emailSent ? 'Email sent on ' + formatDate(edit.emailSentAt) : 'No confirmation email sent'"
>
<fa-icon icon="envelope"></fa-icon>
{{ edit.emailSent ? 'Sent' : 'Not Sent' }}
</span>
@if (edit.emailSent) {
<span class="pce-email-badge"
[class.pce-email-badge--seen]="edit.emailOpenedAt"
[class.pce-email-badge--notseen]="!edit.emailOpenedAt"
[title]="edit.emailOpenedAt ? 'Opened on ' + formatDate(edit.emailOpenedAt) : 'Not yet opened'">
<span
class="pce-email-badge"
[class.pce-email-badge--seen]="edit.emailOpenedAt"
[class.pce-email-badge--notseen]="!edit.emailOpenedAt"
[title]="edit.emailOpenedAt ? 'Opened on ' + formatDate(edit.emailOpenedAt) : 'Not yet opened'"
>
<fa-icon [icon]="edit.emailOpenedAt ? 'eye' : 'eye-slash'"></fa-icon>
{{ edit.emailOpenedAt ? 'Seen' : 'Not Seen' }}
</span>
@@ -157,8 +161,12 @@
</button>
}
@if (edit.emailSent) {
<button mat-stroked-button class="pce-btn pce-btn-email" (click)="viewEmail(edit)"
title="View the exact confirmation email sent to the client">
<button
mat-stroked-button
class="pce-btn pce-btn-email"
(click)="viewEmail(edit)"
title="View the exact confirmation email sent to the client"
>
<fa-icon icon="envelope" class="m-r-10"></fa-icon>View Email
</button>
}
@@ -360,27 +360,31 @@ $text-muted: #64748b;
white-space: nowrap;
vertical-align: middle;
fa-icon { font-size: 0.7rem; }
fa-icon {
font-size: 0.7rem;
}
&--sent {
background: #e8f5e9;
color: #1b5e20;
border: 1px solid #c8e6c9;
}
&--nosent {
background: #f5f5f5;
color: #9e9e9e;
border: 1px solid #e0e0e0;
}
&--seen {
background: #e3f2fd;
color: #0d47a1;
border: 1px solid #bbdefb;
}
&--notseen {
background: #fff3e0;
color: #e65100;
border: 1px solid #ffe0b2;
}
}
@@ -454,8 +454,15 @@ export class PendingClientEditsComponent implements OnInit {
this.dialog.open(ClientEditEmailDialogComponent, {
width: '800px',
maxWidth: '96vw',
data: { emailSubject: null, emailBody: null, emailSent: false,
emailSentAt: null, emailOpenedAt: null, emailFrom: null, emailTo: null }
data: {
emailSubject: null,
emailBody: null,
emailSent: false,
emailSentAt: null,
emailOpenedAt: null,
emailFrom: null,
emailTo: null
}
});
} catch (e: any) {
console.error('Dialog open failed (error): ', e);
+16 -15
View File
@@ -146,8 +146,7 @@ export function declarationStatement(fy: string, asAt: string = TDS_AS_AT): Decl
for (const row of declared) {
const acceptedOn = row.declarationAcceptedOn as string;
const key =
cfg.declarationStatementFrequency === 'MONTHLY' ? acceptedOn.slice(0, 7) : `Q${quarterOf(acceptedOn)}`;
const key = cfg.declarationStatementFrequency === 'MONTHLY' ? acceptedOn.slice(0, 7) : `Q${quarterOf(acceptedOn)}`;
if (!row.declarationUin) {
missingUinCount++;
@@ -182,20 +181,22 @@ export function declarationStatement(fy: string, asAt: string = TDS_AS_AT): Decl
const periods: DeclarationStatementPeriod[] = [...buckets.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([
key,
lines
]) => {
const dueDate = declarationStatementDueDate(lines[0].acceptedOn, year);
return {
.map(
([
key,
label: cfg.declarationStatementFrequency === 'MONTHLY' ? monthLabel(`${key}-01`) : key,
dueDate,
overdue: dueDate < asAt,
lines,
totalPaidWithoutDeduction: lines.reduce((s, l) => s + l.paidWithoutDeduction, 0)
};
});
lines
]) => {
const dueDate = declarationStatementDueDate(lines[0].acceptedOn, year);
return {
key,
label: cfg.declarationStatementFrequency === 'MONTHLY' ? monthLabel(`${key}-01`) : key,
dueDate,
overdue: dueDate < asAt,
lines,
totalPaidWithoutDeduction: lines.reduce((s, l) => s + l.paidWithoutDeduction, 0)
};
}
);
return {
fy: year,