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">
<div class="fm-dialog__header" style="display: flex; align-items: center; gap: 8px;"> <div class="fm-dialog__header" style="display: flex; align-items: center; gap: 8px;">
<mat-icon class="fm-dialog__icon">people</mat-icon> <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 { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms'; 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 * 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. * 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); return this.http.post(`/kyc/exceptions/${clientId}/contact-log`, data);
} }
@@ -442,7 +442,9 @@ export class CreateClientComponent {
}); });
} else { } else {
const { kycType, ...updatePayload } = this.clientKycStep.kycData; 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 // Upload photo & signature
@@ -84,7 +84,7 @@
<mat-icon>phone_iphone</mat-icon> <mat-icon>phone_iphone</mat-icon>
<span>Mobile Number</span> <span>Mobile Number</span>
</div> </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 <mat-checkbox
[checked]="oldMobileNotAccessible" [checked]="oldMobileNotAccessible"
(change)="onOldMobileNotAccessibleToggle($event.checked)" (change)="onOldMobileNotAccessibleToggle($event.checked)"
@@ -103,13 +103,27 @@
(stateChange)="onOldMobileOtpState($event)" (stateChange)="onOldMobileOtpState($event)"
></mifosx-client-contact-otp-block> ></mifosx-client-contact-otp-block>
} @else { } @else {
<div class="fir-upload-box" style="padding: 12px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #f8fafc;"> <div
<div style="font-weight: 500; font-size: 13px; margin-bottom: 6px; color: #475569;"> 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 Upload Police Complaint / FIR Copy for Lost Mobile
</div> </div>
<div style="display: flex; align-items: center; gap: 12px;"> <div style="display: flex; align-items: center; gap: 12px">
<input type="file" (change)="onFirFileSelected($event)" accept=".pdf,.jpg,.jpeg,.png" #firFileInput hidden /> <input
<button mat-stroked-button type="button" (click)="firFileInput.click()" [disabled]="uploadingFir"> 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> <mat-icon>upload_file</mat-icon>
{{ firFileName ? firFileName : 'Select FIR Document' }} {{ firFileName ? firFileName : 'Select FIR Document' }}
</button> </button>
@@ -117,8 +131,18 @@
<mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner> <mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
} }
@if (firDocumentId && !uploadingFir) { @if (firDocumentId && !uploadingFir) {
<span style="color: #16a34a; font-size: 12px; font-weight: 500; display: flex; align-items: center; gap: 4px;"> <span
<mat-icon style="font-size: 16px; width: 16px; height: 16px;">check_circle</mat-icon> FIR Uploaded Successfully 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> </span>
} }
</div> </div>
@@ -228,9 +228,7 @@ export class EditClientComponent {
get contactVerificationComplete(): boolean { get contactVerificationComplete(): boolean {
const changes = this.contactChanges; const changes = this.contactChanges;
if (changes.mobileChanged) { if (changes.mobileChanged) {
const isOldSatisfied = this.oldMobileNotAccessible const isOldSatisfied = this.oldMobileNotAccessible ? this.firDocumentId != null : this.oldMobileVerified;
? (this.firDocumentId != null)
: (this.oldMobileVerified);
if (!isOldSatisfied || !this.newMobileVerified) { if (!isOldSatisfied || !this.newMobileVerified) {
return false; return false;
} }
@@ -753,8 +751,10 @@ export class EditClientComponent {
const oldMobileNorm = normPhone(oldMobile); const oldMobileNorm = normPhone(oldMobile);
// Mobile/Email are considered changed ONLY when the control has been edited (dirty) AND normalized values differ // 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 mobileChanged =
const emailChanged = !!oldEmail && !!newEmail && !!emailControl?.dirty && (newEmail.toLowerCase() !== oldEmail.toLowerCase()); !!oldMobileNorm && !!newMobileNorm && !!mobileControl?.dirty && newMobileNorm !== oldMobileNorm;
const emailChanged =
!!oldEmail && !!newEmail && !!emailControl?.dirty && newEmail.toLowerCase() !== oldEmail.toLowerCase();
return { return {
nameChanged: newName !== oldName, nameChanged: newName !== oldName,
+209
View File
@@ -363,6 +363,10 @@
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA <fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
<fa-icon icon="caret-down" class="ni-caret"></fa-icon> <fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a> </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'"> <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="hand-holding-usd" class="ni-ico"></fa-icon>Advances
<fa-icon icon="caret-down" class="ni-caret"></fa-icon> <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="university" class="ni-ico"></fa-icon>RBI
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon> <fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a> </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'"> <a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu" *mifosxHasPermission="'MENU_DIGITAL'">
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital <fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
<fa-icon icon="caret-down" class="ni-caret"></fa-icon> <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="piggy-bank" class="ni-ico"></fa-icon>Deposits
<fa-icon icon="caret-down" class="ni-caret"></fa-icon> <fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a> </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'"> <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="hand-holding-usd" class="ni-ico"></fa-icon>Advances
<fa-icon icon="caret-down" class="ni-caret"></fa-icon> <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="university" class="ni-ico"></fa-icon>RBI
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon> <fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a> </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'"> <a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu" *mifosxHasPermission="'MENU_REPORTS'">
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports <fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
<fa-icon icon="caret-down" class="ni-caret"></fa-icon> <fa-icon icon="caret-down" class="ni-caret"></fa-icon>
@@ -1271,6 +1292,39 @@
</div> </div>
</mat-menu> </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"> <mat-menu #ckycMenu="matMenu" [overlapTrigger]="false" class="nav-dropdown">
<button mat-menu-item routerLink="/ckyc" tabindex="0"> <button mat-menu-item routerLink="/ckyc" tabindex="0">
<fa-icon icon="id-card" class="menu-item-icon"></fa-icon> CKYC Management <fa-icon icon="id-card" class="menu-item-icon"></fa-icon> CKYC Management
@@ -1859,6 +1913,161 @@
</div> </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 --> <!-- Progress Bar -->
@if (progressBarMode !== 'none') { @if (progressBarMode !== 'none') {
<div> <div>
@@ -771,27 +771,39 @@
<div class="km-card km-mt"> <div class="km-card km-mt">
<!-- Tab Toggle Header --> <!-- 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
<div style="display: flex; gap: 8px; align-items: center;"> class="km-ch"
<div class="km-ci" style="background: #fef3c7; color: #d97706;"><fa-icon icon="exclamation-triangle"></fa-icon></div> 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 class="km-ct">Exceptions Register</div>
</div> </div>
<div class="km-inner-nav" style="width: 100%; border: none; margin: 10px 0 0 0; padding: 0; gap: 8px;"> <div class="km-inner-nav" style="width: 100%; border: none; margin: 10px 0 0 0; padding: 0; gap: 8px">
<button <button
class="km-itab" class="km-itab"
[class.active]="activeExceptionSubTab === 'pending'" [class.active]="activeExceptionSubTab === 'pending'"
(click)="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> <fa-icon icon="clock"></fa-icon>
Pending Exceptions ({{ pendingExceptions.length }}) Pending Exceptions ({{ pendingExceptions.length }})
</button> </button>
<button <button
class="km-itab" class="km-itab"
[class.active]="activeExceptionSubTab === 'resolved'" [class.active]="activeExceptionSubTab === 'resolved'"
(click)="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> <fa-icon icon="check-circle"></fa-icon>
Resolved Exceptions ({{ resolvedExceptions.length }}) Resolved Exceptions ({{ resolvedExceptions.length }})
@@ -802,10 +814,12 @@
<!-- PENDING EXCEPTIONS TAB --> <!-- PENDING EXCEPTIONS TAB -->
@if (activeExceptionSubTab === 'pending') { @if (activeExceptionSubTab === 'pending') {
@if (pendingExceptions.length === 0) { @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 class="km-empty-i">🎉</div>
<div style="font-weight: 500; font-size: 16px; margin-top: 12px;">All exceptions resolved!</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 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> </div>
} @else { } @else {
<table class="km-tbl"> <table class="km-tbl">
@@ -824,12 +838,18 @@
@for (c of pendingExceptions; track c.id) { @for (c of pendingExceptions; track c.id) {
<tr> <tr>
<td> <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> <div class="km-ai">{{ c.account }}</div>
</td> </td>
<td>{{ c.contact.branch }}</td> <td>{{ c.contact.branch }}</td>
<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> <div class="km-small">Opened: {{ c.exceptionDate }}</div>
</td> </td>
<td> <td>
@@ -838,20 +858,25 @@
<td> <td>
@if (c.contactHistory && c.contactHistory.length > 0) { @if (c.contactHistory && c.contactHistory.length > 0) {
<span class="km-del d-ok">Contacted</span> <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 { } @else {
<span class="km-del d-fa">Not Contacted</span> <span class="km-del d-fa">Not Contacted</span>
} }
</td> </td>
<td> <td>
@if (c.contactHistory && c.contactHistory.length > 0) { @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) { @for (log of c.contactHistory; track $index) {
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0;"> <div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0">
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }} <em>— {{ log.officer }}</em> <strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }}
<em>— {{ log.officer }}</em>
@if (log.platformSent) { @if (log.platformSent) {
@if (log.delivered) { @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 { } @else {
<span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span> <span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span>
} }
@@ -860,12 +885,16 @@
} }
</div> </div>
} @else { } @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>
<td> <td>
<div style="display: flex; gap: 8px;"> <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']"> <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 <fa-icon icon="user-check"></fa-icon> Do KYC
</button> </button>
<button class="km-btn km-btn-ot" (click)="openContactDialog(c)"> <button class="km-btn km-btn-ot" (click)="openContactDialog(c)">
@@ -883,10 +912,14 @@
<!-- RESOLVED EXCEPTIONS TAB --> <!-- RESOLVED EXCEPTIONS TAB -->
@if (activeExceptionSubTab === 'resolved') { @if (activeExceptionSubTab === 'resolved') {
@if (resolvedExceptions.length === 0) { @if (resolvedExceptions.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" style="font-size: 32px; filter: grayscale(1);">📋</div> <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 style="font-weight: 500; font-size: 16px; margin-top: 12px; color: var(--km-g500)">
<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> 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> </div>
} @else { } @else {
<table class="km-tbl"> <table class="km-tbl">
@@ -904,12 +937,20 @@
@for (c of resolvedExceptions; track c.id) { @for (c of resolvedExceptions; track c.id) {
<tr> <tr>
<td> <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> <div class="km-ai">{{ c.account }}</div>
</td> </td>
<td>{{ c.contact.branch }}</td> <td>{{ c.contact.branch }}</td>
<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> <div class="km-small">Resolved On: {{ c.exceptionResolvedOn }}</div>
</td> </td>
<td> <td>
@@ -917,13 +958,16 @@
</td> </td>
<td> <td>
@if (c.contactHistory && c.contactHistory.length > 0) { @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) { @for (log of c.contactHistory; track $index) {
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0;"> <div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0">
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }} <em>— {{ log.officer }}</em> <strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }}
<em>— {{ log.officer }}</em>
@if (log.platformSent) { @if (log.platformSent) {
@if (log.delivered) { @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 { } @else {
<span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span> <span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span>
} }
@@ -932,11 +976,23 @@
} }
</div> </div>
} @else { } @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>
<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 <fa-icon icon="check-circle"></fa-icon> Exception Resolved
</span> </span>
</td> </td>
@@ -1421,7 +1477,9 @@
<label class="hv-label">Contact Channel<span class="hv-req">*</span></label> <label class="hv-label">Contact Channel<span class="hv-req">*</span></label>
<select class="hv-input" [(ngModel)]="contactChannel"> <select class="hv-input" [(ngModel)]="contactChannel">
@for (ch of contactChannels; track ch.value) { @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> </select>
@if (selectedChannelSends) { @if (selectedChannelSends) {
@@ -1430,14 +1488,12 @@
A KYC notice will be sent to the customer when you save. A KYC notice will be sent to the customer when you save.
</div> </div>
} @else { } @else {
<div class="hv-hint"> <div class="hv-hint">Records an action you already took. Nothing is sent to the customer.</div>
Records an action you already took. Nothing is sent to the customer.
</div>
} }
@if (selectedChannelUnavailable) { @if (selectedChannelUnavailable) {
<div class="hv-hint hv-hint-error"> <div class="hv-hint hv-hint-error">
This customer has no {{ selectedChannel?.requires === 'email' ? 'email address' : 'mobile number' }} on file — This customer has no {{ selectedChannel?.requires === 'email' ? 'email address' : 'mobile number' }} on
choose a different channel. file — choose a different channel.
</div> </div>
} }
</div> </div>
@@ -1470,11 +1526,7 @@
<div class="hv-footer"> <div class="hv-footer">
<button class="km-btn" [disabled]="contactSaving" (click)="closeContactDialog()">Cancel</button> <button class="km-btn" [disabled]="contactSaving" (click)="closeContactDialog()">Cancel</button>
<button <button class="km-btn km-btn-t" [disabled]="contactSaveDisabled" (click)="saveContactLog()">
class="km-btn km-btn-t"
[disabled]="contactSaveDisabled"
(click)="saveContactLog()"
>
{{ contactSaveLabel }} {{ contactSaveLabel }}
</button> </button>
</div> </div>
@@ -1486,8 +1538,8 @@
@if (resolveDialogOpen && current) { @if (resolveDialogOpen && current) {
<div class="km-overlay" (click)="closeResolveDialog()"> <div class="km-overlay" (click)="closeResolveDialog()">
<div class="km-modal hv-modal" (click)="$event.stopPropagation()"> <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" 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-av" style="background: var(--km-green)">{{ current.av }}</div>
<div> <div>
<div class="km-mh-name">Resolve KYC Exception</div> <div class="km-mh-name">Resolve KYC Exception</div>
<div class="km-mh-meta">{{ current.name }} · {{ current.account }}</div> <div class="km-mh-meta">{{ current.name }} · {{ current.account }}</div>
@@ -1496,25 +1548,52 @@
</div> </div>
<div class="km-mbody hv-body"> <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 }} <strong>Pending Reason:</strong> {{ current.exceptionReason }}
@if (current.exceptionRemarks) { @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> </div>
@if (showPanResolution) { @if (showPanResolution) {
<div style="border: 1px solid var(--km-g300); border-radius: 6px; padding: 12px; margin-bottom: 16px; background: #fafafa;"> <div
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> 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" /> <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> </div>
@if (resolvePanActive) { @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> <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) { @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> </div>
} }
@@ -1522,17 +1601,33 @@
} }
@if (showAadhaarResolution) { @if (showAadhaarResolution) {
<div style="border: 1px solid var(--km-g300); border-radius: 6px; padding: 12px; margin-bottom: 16px; background: #fafafa;"> <div
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> 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" /> <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> </div>
@if (resolveAadhaarActive) { @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> <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) { @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> </div>
} }
@@ -1540,13 +1635,23 @@
} }
@if (showGeneralResolution) { @if (showGeneralResolution) {
<div style="border: 1px solid var(--km-g300); border-radius: 6px; padding: 12px; margin-bottom: 16px; background: #fafafa;"> <div
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> 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" /> <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> </div>
@if (resolveGeneralActive) { @if (resolveGeneralActive) {
<div style="margin-left: 20px;"> <div style="margin-left: 20px">
<div class="hv-row"> <div class="hv-row">
<label class="hv-label">Verification Document Type<span class="hv-req">*</span></label> <label class="hv-label">Verification Document Type<span class="hv-req">*</span></label>
<select class="hv-input" [(ngModel)]="resolveGeneralDocumentType"> <select class="hv-input" [(ngModel)]="resolveGeneralDocumentType">
@@ -1555,7 +1660,12 @@
</div> </div>
<div class="hv-row"> <div class="hv-row">
<label class="hv-label">Document Identification Number<span class="hv-req">*</span></label> <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>
</div> </div>
} }
@@ -1575,11 +1685,7 @@
<div class="hv-footer"> <div class="hv-footer">
<button class="km-btn" (click)="closeResolveDialog()">Cancel</button> <button class="km-btn" (click)="closeResolveDialog()">Cancel</button>
<button <button class="km-btn km-btn-g" [disabled]="!isResolveValid" (click)="saveResolveKyc()">
class="km-btn km-btn-g"
[disabled]="!isResolveValid"
(click)="saveResolveKyc()"
>
Complete KYC &amp; Resolve Exception Complete KYC &amp; Resolve Exception
</button> </button>
</div> </div>
@@ -1590,4 +1696,3 @@
<!-- TOAST --> <!-- TOAST -->
<div class="km-toast" [class.show]="toastVisible">{{ toastMsg }}</div> <div class="km-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div> </div>
@@ -9,22 +9,16 @@
<h1 mat-dialog-title class="ced-title"> <h1 mat-dialog-title class="ced-title">
<span class="ced-title__icon">✉</span> <span class="ced-title__icon">✉</span>
Approval Email Approval Email
<div class="ced-status-row"> <div class="ced-status-row">
@if (data.emailSent) { @if (data.emailSent) {
<span class="ced-chip ced-chip--sent"> <span class="ced-chip ced-chip--sent"> <span class="ced-chip__dot"></span>Sent </span>
<span class="ced-chip__dot"></span>Sent
</span>
} @else { } @else {
<span class="ced-chip ced-chip--notsent"> <span class="ced-chip ced-chip--notsent"> <span class="ced-chip__dot"></span>Not sent </span>
<span class="ced-chip__dot"></span>Not sent
</span>
} }
@if (data.emailOpenedAt) { @if (data.emailOpenedAt) {
<span class="ced-chip ced-chip--seen"> <span class="ced-chip ced-chip--seen"> <fa-icon icon="eye" class="ced-chip__icon"></fa-icon>Seen </span>
<fa-icon icon="eye" class="ced-chip__icon"></fa-icon>Seen
</span>
} @else { } @else {
<span class="ced-chip ced-chip--notseen"> <span class="ced-chip ced-chip--notseen">
<fa-icon icon="eye-slash" class="ced-chip__icon"></fa-icon>Not yet seen <fa-icon icon="eye-slash" class="ced-chip__icon"></fa-icon>Not yet seen
@@ -34,16 +28,15 @@
</h1> </h1>
<div mat-dialog-content class="ced-content"> <div mat-dialog-content class="ced-content">
<!-- Classic Email Header (Gmail Style) --> <!-- Classic Email Header (Gmail Style) -->
<div class="ced-gmail-header"> <div class="ced-gmail-header">
<h2 class="ced-gmail-subject">{{ data.emailSubject ?? '(No Subject)' }}</h2> <h2 class="ced-gmail-subject">{{ data.emailSubject ?? '(No Subject)' }}</h2>
<div class="ced-gmail-meta"> <div class="ced-gmail-meta">
<div class="ced-gmail-avatar"> <div class="ced-gmail-avatar">
<div class="ced-gmail-avatar-letter">C</div> <div class="ced-gmail-avatar-letter">C</div>
</div> </div>
<div class="ced-gmail-meta-content"> <div class="ced-gmail-meta-content">
<div class="ced-gmail-meta-top"> <div class="ced-gmail-meta-top">
<span class="ced-gmail-sender-name">CBSNexora</span> <span class="ced-gmail-sender-name">CBSNexora</span>
@@ -53,7 +46,7 @@
</div> </div>
<div class="ced-gmail-meta-bottom"> <div class="ced-gmail-meta-bottom">
<span class="ced-gmail-to"> <span class="ced-gmail-to">
to to
@if (data.emailTo) { @if (data.emailTo) {
{{ data.emailTo }} {{ data.emailTo }}
} @else { } @else {
@@ -1,6 +1,9 @@
/** /**
* Copyright since 2025 Mifos Initiative * 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 { :host {
@@ -35,14 +38,14 @@
flex-direction: column; flex-direction: column;
margin-bottom: 30px; margin-bottom: 30px;
padding: 0 4px; padding: 0 4px;
font-family: 'Roboto', 'Inter', sans-serif; /* Gmail-like typography */ font-family: Roboto, Inter, sans-serif; /* Gmail-like typography */
} }
.ced-gmail-subject { .ced-gmail-subject {
font-size: 1.35rem; font-size: 1.35rem;
font-weight: 400; font-weight: 400;
color: #202124; color: #202124;
margin: 0 0 24px 0; margin: 0 0 24px;
line-height: 1.4; line-height: 1.4;
} }
@@ -153,13 +156,19 @@
&--sent { &--sent {
background: #e8f5e9; background: #e8f5e9;
color: #1b5e20; color: #1b5e20;
.ced-chip__dot { background: #43a047; }
.ced-chip__dot {
background: #43a047;
}
} }
&--notsent { &--notsent {
background: #f5f5f5; background: #f5f5f5;
color: #757575; color: #757575;
.ced-chip__dot { background: #bdbdbd; }
.ced-chip__dot {
background: #bdbdbd;
}
} }
&--seen { &--seen {
@@ -176,7 +185,7 @@
/* ── Divider ────────────────────────────────────────────────────────────── */ /* ── Divider ────────────────────────────────────────────────────────────── */
.ced-divider { .ced-divider {
border: none; border: none;
border-top: 1px solid #eeeeee; border-top: 1px solid #eee;
margin: 12px 0; margin: 12px 0;
} }
@@ -197,13 +206,19 @@
width: 100%; width: 100%;
} }
td, th { td,
th {
padding: 6px 12px; padding: 6px 12px;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
} }
p { margin: 0 0 10px; } p {
ul { padding-left: 20px; } margin: 0 0 10px;
}
ul {
padding-left: 20px;
}
} }
/* ── Empty state ────────────────────────────────────────────────────────── */ /* ── Empty state ────────────────────────────────────────────────────────── */
@@ -58,8 +58,12 @@ export class ClientEditEmailDialogComponent {
if (!isoDate) return '—'; if (!isoDate) return '—';
const d = new Date(isoDate); const d = new Date(isoDate);
return d.toLocaleString('en-IN', { return d.toLocaleString('en-IN', {
day: '2-digit', month: 'short', year: 'numeric', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: true month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}); });
} }
} }
@@ -127,18 +127,22 @@
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let edit"> <td mat-cell *matCellDef="let edit">
<!-- ── Email status badges (compact, always visible) ── --> <!-- ── Email status badges (compact, always visible) ── -->
<span class="pce-email-badge" <span
[class.pce-email-badge--sent]="edit.emailSent" class="pce-email-badge"
[class.pce-email-badge--nosent]="!edit.emailSent" [class.pce-email-badge--sent]="edit.emailSent"
[title]="edit.emailSent ? 'Email sent on ' + formatDate(edit.emailSentAt) : 'No confirmation email sent'"> [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> <fa-icon icon="envelope"></fa-icon>
{{ edit.emailSent ? 'Sent' : 'Not Sent' }} {{ edit.emailSent ? 'Sent' : 'Not Sent' }}
</span> </span>
@if (edit.emailSent) { @if (edit.emailSent) {
<span class="pce-email-badge" <span
[class.pce-email-badge--seen]="edit.emailOpenedAt" class="pce-email-badge"
[class.pce-email-badge--notseen]="!edit.emailOpenedAt" [class.pce-email-badge--seen]="edit.emailOpenedAt"
[title]="edit.emailOpenedAt ? 'Opened on ' + formatDate(edit.emailOpenedAt) : 'Not yet opened'"> [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> <fa-icon [icon]="edit.emailOpenedAt ? 'eye' : 'eye-slash'"></fa-icon>
{{ edit.emailOpenedAt ? 'Seen' : 'Not Seen' }} {{ edit.emailOpenedAt ? 'Seen' : 'Not Seen' }}
</span> </span>
@@ -157,8 +161,12 @@
</button> </button>
} }
@if (edit.emailSent) { @if (edit.emailSent) {
<button mat-stroked-button class="pce-btn pce-btn-email" (click)="viewEmail(edit)" <button
title="View the exact confirmation email sent to the client"> 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 <fa-icon icon="envelope" class="m-r-10"></fa-icon>View Email
</button> </button>
} }
@@ -360,27 +360,31 @@ $text-muted: #64748b;
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
fa-icon { font-size: 0.7rem; } fa-icon {
font-size: 0.7rem;
}
&--sent { &--sent {
background: #e8f5e9; background: #e8f5e9;
color: #1b5e20; color: #1b5e20;
border: 1px solid #c8e6c9; border: 1px solid #c8e6c9;
} }
&--nosent { &--nosent {
background: #f5f5f5; background: #f5f5f5;
color: #9e9e9e; color: #9e9e9e;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
} }
&--seen { &--seen {
background: #e3f2fd; background: #e3f2fd;
color: #0d47a1; color: #0d47a1;
border: 1px solid #bbdefb; border: 1px solid #bbdefb;
} }
&--notseen { &--notseen {
background: #fff3e0; background: #fff3e0;
color: #e65100; color: #e65100;
border: 1px solid #ffe0b2; border: 1px solid #ffe0b2;
} }
} }
@@ -454,8 +454,15 @@ export class PendingClientEditsComponent implements OnInit {
this.dialog.open(ClientEditEmailDialogComponent, { this.dialog.open(ClientEditEmailDialogComponent, {
width: '800px', width: '800px',
maxWidth: '96vw', maxWidth: '96vw',
data: { emailSubject: null, emailBody: null, emailSent: false, data: {
emailSentAt: null, emailOpenedAt: null, emailFrom: null, emailTo: null } emailSubject: null,
emailBody: null,
emailSent: false,
emailSentAt: null,
emailOpenedAt: null,
emailFrom: null,
emailTo: null
}
}); });
} catch (e: any) { } catch (e: any) {
console.error('Dialog open failed (error): ', e); 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) { for (const row of declared) {
const acceptedOn = row.declarationAcceptedOn as string; const acceptedOn = row.declarationAcceptedOn as string;
const key = const key = cfg.declarationStatementFrequency === 'MONTHLY' ? acceptedOn.slice(0, 7) : `Q${quarterOf(acceptedOn)}`;
cfg.declarationStatementFrequency === 'MONTHLY' ? acceptedOn.slice(0, 7) : `Q${quarterOf(acceptedOn)}`;
if (!row.declarationUin) { if (!row.declarationUin) {
missingUinCount++; missingUinCount++;
@@ -182,20 +181,22 @@ export function declarationStatement(fy: string, asAt: string = TDS_AS_AT): Decl
const periods: DeclarationStatementPeriod[] = [...buckets.entries()] const periods: DeclarationStatementPeriod[] = [...buckets.entries()]
.sort((a, b) => a[0].localeCompare(b[0])) .sort((a, b) => a[0].localeCompare(b[0]))
.map(([ .map(
key, ([
lines
]) => {
const dueDate = declarationStatementDueDate(lines[0].acceptedOn, year);
return {
key, key,
label: cfg.declarationStatementFrequency === 'MONTHLY' ? monthLabel(`${key}-01`) : key, lines
dueDate, ]) => {
overdue: dueDate < asAt, const dueDate = declarationStatementDueDate(lines[0].acceptedOn, year);
lines, return {
totalPaidWithoutDeduction: lines.reduce((s, l) => s + l.paidWithoutDeduction, 0) key,
}; label: cfg.declarationStatementFrequency === 'MONTHLY' ? monthLabel(`${key}-01`) : key,
}); dueDate,
overdue: dueDate < asAt,
lines,
totalPaidWithoutDeduction: lines.reduce((s, l) => s + l.paidWithoutDeduction, 0)
};
}
);
return { return {
fy: year, fy: year,