feat: Client & CKYC UI improvements — filters, KYC management, image crop+merge

- Clients list: CKYC icon, CKYC Number column, status/legal form/CKYC filters
- CKYC: SFTP upload logs, date formatting, modal z-index fix, non-dismissible dialogs
- KYC Management: new standalone module with alert dashboard, notification scheduler, audit inventory, account status
- Image cropper: ngx-image-cropper integration for front+back document merge into single PNG
- Sidebar: KYC Management menu entry after CKYC

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-16 13:51:11 +05:30
co-authored by Claude Opus 4.6
parent 79e455c4ed
commit d72e969dc3
210 changed files with 14273 additions and 1139 deletions
@@ -0,0 +1,485 @@
/**
* 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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
interface KmNotice {
no: string;
channel: string;
date: string;
ref: string;
delivery: string;
response: string;
}
interface KmEvidence {
type: 'visit' | 'vkyc';
date: string;
officer: string;
gps?: string;
location?: string;
ref?: string;
outcome: string;
docket?: string;
}
interface KmClient {
id: string;
av: string;
name: string;
meta: string;
state: 'alert' | 'freeze' | 'inop' | 'dormant' | 'urgent';
risk: 'low' | 'mid' | 'high';
riskLabel: string;
expiry: string;
daysLeft: number;
account: string;
kyc: {
type: string;
initiated: string;
lastVerified: string;
validity: string;
pan: string;
aadhaar: string;
panStatus: string;
aadhaarStatus: string;
panLink: string;
};
contact: {
mobile: string;
email: string;
address: string;
branch: string;
};
notices: KmNotice[];
noticesSent: number;
noticePending: string;
nextStage: string;
pendingActions: string[];
actionTaken: string[];
evidence: KmEvidence | null;
}
@Component({
selector: 'mifosx-kyc-management',
templateUrl: './kyc-management.component.html',
styleUrls: ['./kyc-management.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
FaIconComponent
]
})
export class KycManagementComponent {
activeSection: 'alerts' | 'notif' | 'audit' | 'status' = 'alerts';
modalOpen = false;
modalTab: 'overview' | 'timeline' | 'notices' | 'evidence' | 'action' = 'overview';
current: KmClient | null = null;
toastMsg = '';
toastVisible = false;
private toastTimer: any;
clients: Record<string, KmClient> = {
rajesh: {
id: 'rajesh',
av: 'RS',
name: 'Rajesh Kumar Sharma',
meta: 'ACC-BPL-2024-00847 · Savings A/c · Bhopal Main',
account: 'ACC-BPL-2024-00847',
state: 'alert',
risk: 'low',
riskLabel: 'Low Risk',
expiry: '30 Apr 2027',
daysLeft: 15,
kyc: {
type: 'Individual',
initiated: '4 Apr 2026',
lastVerified: '4 Apr 2026',
validity: '10 Years',
pan: 'AQRPV8345E',
aadhaar: 'XXXXXXXX6039',
panStatus: 'Verified',
aadhaarStatus: 'Verified',
panLink: 'Linked'
},
contact: {
mobile: '+91-XXXXX-99041',
email: 'r****@gmail.com',
address: 'House No. 42, Sector 5, Bhopal MP 462001',
branch: 'Bhopal Main'
},
notices: [
{ no: 'N1', channel: 'SMS + Email', date: '31 Mar 2027', ref: 'MSG-2027-0331-001', delivery: 'Delivered', response: 'No Action' },
{ no: 'N2', channel: 'Reg. Post + IVR', date: '10 Apr 2027', ref: 'RP-BPL-20270410-443', delivery: 'In Transit', response: 'Pending' }
],
noticesSent: 2,
noticePending: 'Notice 3',
nextStage: 'Debit Freeze',
pendingActions: ['Send Notice 3 (due 20 Apr 2027)', 'Schedule Branch Visit', 'Send Video KYC link'],
actionTaken: ['Notice 1 delivered', 'Notice 2 dispatched via Reg. Post'],
evidence: null
},
priya: {
id: 'priya',
av: 'PM',
name: 'Priya Malhotra',
meta: 'ACC-BPL-2021-00231 · Savings A/c · Bhopal Main',
account: 'ACC-BPL-2021-00231',
state: 'freeze',
risk: 'mid',
riskLabel: 'Mid Risk',
expiry: '10 Apr 2027',
daysLeft: 0,
kyc: {
type: 'Individual',
initiated: '10 Apr 2019',
lastVerified: '10 Apr 2019',
validity: '8 Years',
pan: 'BKJPV1234A',
aadhaar: 'XXXXXXXX7721',
panStatus: 'Verified',
aadhaarStatus: 'Verified',
panLink: 'Linked'
},
contact: {
mobile: '+91-XXXXX-55321',
email: 'p****@yahoo.com',
address: 'Flat 12B, Arera Colony, Bhopal MP 462016',
branch: 'Bhopal Main'
},
notices: [
{ no: 'N1', channel: 'SMS + Email', date: '10 Mar 2027', ref: 'MSG-2027-0310-003', delivery: 'Delivered', response: 'No Response' },
{ no: 'N2', channel: 'Reg. Post', date: '20 Mar 2027', ref: 'RP-BPL-20270320-091', delivery: 'Delivered', response: 'No Response' },
{ no: 'N3', channel: 'Branch Visit', date: '30 Mar 2027', ref: 'VIS-BPL-20270330-012', delivery: 'Visit Done', response: 'Refused' }
],
noticesSent: 3,
noticePending: 'None — all 3 sent',
nextStage: 'Inoperative by 30 Jun 2027',
pendingActions: ['Account in Debit Freeze', 'Inoperative transition due 30 Jun 2027', 'Maker-checker unfreeze pending'],
actionTaken: ['Notice 1 delivered', 'Notice 2 delivered via Reg. Post', 'Notice 3 — Branch visit done, client refused', 'Debit Freeze applied 10 Apr 2027'],
evidence: {
type: 'visit',
date: '30 Mar 2027',
officer: 'Anupam Mishra',
gps: '23.2599° N, 77.4126° E',
location: 'Arera Colony, Bhopal',
outcome: 'Client refused to sign self-declaration form. Informed about debit freeze implications.',
docket: 'VIS-BPL-20270330-012'
}
},
suresh: {
id: 'suresh',
av: 'SP',
name: 'Suresh Patel',
meta: 'ACC-BPL-2019-00112 · Savings A/c · Bhopal South',
account: 'ACC-BPL-2019-00112',
state: 'inop',
risk: 'high',
riskLabel: 'High Risk',
expiry: '15 Mar 2027',
daysLeft: 0,
kyc: {
type: 'Individual',
initiated: '15 Mar 2025',
lastVerified: '15 Mar 2025',
validity: '2 Years',
pan: 'CXRTS5678B',
aadhaar: 'XXXXXXXX2211',
panStatus: 'Verified',
aadhaarStatus: 'Verified',
panLink: 'Linked'
},
contact: {
mobile: '+91-XXXXX-88765',
email: 's****@gmail.com',
address: 'Village Ratibad, Bhopal MP 462044',
branch: 'Bhopal South'
},
notices: [
{ no: 'N1', channel: 'SMS + Email', date: '13 Feb 2027', ref: 'MSG-2027-0213-006', delivery: 'Delivered', response: 'No Response' },
{ no: 'N2', channel: 'Reg. Post + IVR', date: '23 Feb 2027', ref: 'RP-BPL-20270223-044', delivery: 'Delivered', response: 'No Response' },
{ no: 'N3', channel: 'Video KYC', date: '5 Mar 2027', ref: 'VKYC-2027-0305-007', delivery: 'No Response', response: 'No Action' }
],
noticesSent: 3,
noticePending: 'None',
nextStage: 'Dormant by 30 Sep 2027',
pendingActions: ['Account Inoperative — all txns blocked', 'Dormant transition due 30 Sep 2027', 'RBI Dormant Account reporting'],
actionTaken: ['Notice 1 delivered', 'Notice 2 delivered via Reg. Post', 'Notice 3 — Video KYC invite sent (no response)', 'Debit Freeze 15 Mar 2027', 'Inoperative 15 May 2027'],
evidence: {
type: 'vkyc',
date: '5 Mar 2027',
officer: 'System',
ref: 'VKYC-2027-0305-007',
outcome: 'Video KYC session link sent. Client did not join within 48 hours. Session auto-expired.'
}
},
kavitha: {
id: 'kavitha',
av: 'KN',
name: 'Kavitha Nair',
meta: 'ACC-BPL-2018-00088 · Savings A/c · Bhopal East',
account: 'ACC-BPL-2018-00088',
state: 'dormant',
risk: 'mid',
riskLabel: 'Mid Risk',
expiry: '1 Jan 2027',
daysLeft: 0,
kyc: {
type: 'Individual',
initiated: '1 Jan 2019',
lastVerified: '1 Jan 2019',
validity: '8 Years',
pan: 'DVJNK9012C',
aadhaar: 'XXXXXXXX4492',
panStatus: 'Verified',
aadhaarStatus: 'Verified',
panLink: 'Linked'
},
contact: {
mobile: '+91-XXXXX-11092',
email: 'k****@rediff.com',
address: 'Plot 7, Awadhpuri, Bhopal MP 462010',
branch: 'Bhopal East'
},
notices: [
{ no: 'N1', channel: 'SMS + Email', date: '1 Dec 2026', ref: 'MSG-2026-1201-009', delivery: 'Delivered', response: 'No Response' },
{ no: 'N2', channel: 'Reg. Post', date: '11 Dec 2026', ref: 'RP-BPL-20261211-077', delivery: 'Delivered', response: 'No Response' },
{ no: 'N3', channel: 'Branch Visit', date: '22 Dec 2026', ref: 'VIS-BPL-20261222-019', delivery: 'Not Found', response: 'Not Found at Address' }
],
noticesSent: 3,
noticePending: 'None',
nextStage: 'Dormant (Final) — RBI Reporting',
pendingActions: ['RBI Dormant Account Report submission', 'Branch Manager approval for any reversal', 'Regional Manager sign-off required'],
actionTaken: ['Notice 1–3 sent, all no response', 'Debit Freeze 1 Jan 2027', 'Inoperative 2 Mar 2027', 'Dormant 1 Jun 2027', 'RBI report auto-generated'],
evidence: {
type: 'visit',
date: '22 Dec 2026',
officer: 'Rekha Sharma',
gps: '23.2491° N, 77.4384° E',
location: 'Awadhpuri, Bhopal',
outcome: 'Client not found at registered address. Neighbours confirmed relocation. Address verification required.',
docket: 'VIS-BPL-20261222-019'
}
},
amit: {
id: 'amit',
av: 'AS',
name: 'Amit Singh',
meta: 'ACC-BPL-2024-01103 · Savings A/c · Bhopal Main',
account: 'ACC-BPL-2024-01103',
state: 'alert',
risk: 'low',
riskLabel: 'Low Risk',
expiry: '5 May 2027',
daysLeft: 20,
kyc: {
type: 'Individual',
initiated: '5 May 2016',
lastVerified: '5 May 2016',
validity: '10 Years',
pan: 'EKJPS4321D',
aadhaar: 'XXXXXXXX8834',
panStatus: 'Verified',
aadhaarStatus: 'Verified',
panLink: 'Linked'
},
contact: {
mobile: '+91-XXXXX-22334',
email: 'a****@gmail.com',
address: 'C-22, Shivaji Nagar, Bhopal MP 462016',
branch: 'Bhopal Main'
},
notices: [
{ no: 'N1', channel: 'SMS + Email', date: '5 Apr 2027', ref: 'MSG-2027-0405-011', delivery: 'Delivered', response: 'No Action' }
],
noticesSent: 1,
noticePending: 'Notice 2 & 3',
nextStage: 'Debit Freeze if no action',
pendingActions: ['Send Notice 2 (due 15 Apr 2027)', 'Send Notice 3 (due 25 Apr 2027)', 'Branch visit if needed'],
actionTaken: ['Notice 1 delivered 5 Apr 2027'],
evidence: null
},
meena: {
id: 'meena',
av: 'MJ',
name: 'Meena Joshi',
meta: 'ACC-BPL-2022-00567 · Savings A/c · Bhopal North',
account: 'ACC-BPL-2022-00567',
state: 'urgent',
risk: 'high',
riskLabel: 'High Risk',
expiry: '28 Apr 2027',
daysLeft: 3,
kyc: {
type: 'Individual',
initiated: '28 Apr 2025',
lastVerified: '28 Apr 2025',
validity: '2 Years',
pan: 'FJLMN6789E',
aadhaar: 'XXXXXXXX3309',
panStatus: 'Verified',
aadhaarStatus: 'Verified',
panLink: 'Linked'
},
contact: {
mobile: '+91-XXXXX-77654',
email: 'm****@gmail.com',
address: 'Lane 4, Shyamla Hills, Bhopal MP 462002',
branch: 'Bhopal North'
},
notices: [
{ no: 'N1', channel: 'SMS + Email', date: '29 Mar 2027', ref: 'MSG-2027-0329-014', delivery: 'Delivered', response: 'No Action' },
{ no: 'N2', channel: 'Reg. Post + IVR', date: '8 Apr 2027', ref: 'RP-BPL-20270408-088', delivery: 'Delivered', response: 'No Response' }
],
noticesSent: 2,
noticePending: 'URGENT: Notice 3',
nextStage: 'Debit Freeze in 3 days',
pendingActions: ['URGENT: Send Notice 3 NOW', 'Schedule Branch Visit immediately', 'Consider Video KYC as last resort'],
actionTaken: ['Notice 1 delivered', 'Notice 2 delivered via Reg. Post + IVR'],
evidence: null
}
};
alertRows: KmClient[] = [];
ngOnInit(): void {
this.alertRows = Object.values(this.clients);
}
showSection(section: 'alerts' | 'notif' | 'audit' | 'status'): void {
this.activeSection = section;
}
openModal(id: string): void {
const c = this.clients[id];
if (!c) return;
this.current = c;
this.modalTab = 'overview';
this.modalOpen = true;
}
closeModal(): void {
this.modalOpen = false;
this.current = null;
}
showMTab(tab: 'overview' | 'timeline' | 'notices' | 'evidence' | 'action'): void {
this.modalTab = tab;
}
toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) clearTimeout(this.toastTimer);
this.toastTimer = setTimeout(() => (this.toastVisible = false), 2800);
}
/** Compliance journey stepper state for a given client */
getStepperState(c: KmClient): { label: string; state: 'done' | 'pending' | 'current' | 'danger' }[] {
return [
{ label: 'KYC Active', state: 'done' },
{ label: 'Alert Sent', state: c.noticesSent >= 1 ? 'done' : 'pending' },
{ label: 'Notice 2', state: c.noticesSent >= 2 ? 'done' : c.noticesSent >= 1 ? 'current' : 'pending' },
{
label: 'Notice 3',
state:
c.noticesSent >= 3
? 'done'
: c.noticesSent >= 2
? c.state === 'urgent'
? 'danger'
: 'current'
: 'pending'
},
{
label: 'Debit Freeze',
state: ['freeze', 'inop', 'dormant'].includes(c.state) ? 'done' : c.noticesSent >= 3 ? 'danger' : 'pending'
},
{ label: 'Inoperative', state: ['inop', 'dormant'].includes(c.state) ? 'done' : 'pending' },
{ label: 'Dormant', state: c.state === 'dormant' ? 'danger' : 'pending' }
];
}
stepIcon(state: string): string {
if (state === 'done') return '✓';
if (state === 'danger') return '!';
return '·';
}
stateLabel(state: string): string {
return (
{
alert: 'Alert Window',
freeze: 'Debit Freeze',
inop: 'Inoperative',
dormant: 'Dormant',
urgent: 'Urgent Alert'
} as Record<string, string>
)[state] || state;
}
stateClass(state: string): string {
return (
{
alert: 'sp-al',
freeze: 'sp-fr',
inop: 'sp-in',
dormant: 'sp-do',
urgent: 'sp-fr'
} as Record<string, string>
)[state] || 'sp-al';
}
riskClass(risk: string): string {
return ({ low: 'b-gr', mid: 'b-am', high: 'b-re' } as Record<string, string>)[risk] || 'b-gy';
}
/** Notices pending count (cap at 0) */
noticesPending(c: KmClient): number {
return Math.max(0, 3 - c.noticesSent);
}
deliveryClass(d: string): string {
const m: Record<string, string> = {
Delivered: 'd-ok',
'In Transit': 'd-tr',
'No Response': 'd-fa',
Pending: 'd-pe',
'Visit Done': 'd-ok',
'Not Found': 'd-fa'
};
return m[d] || 'd-pe';
}
barWidth(c: KmClient): number {
if (c.daysLeft <= 0) return 100;
return Math.max(2, Math.min(100, c.daysLeft * 3));
}
barColor(c: KmClient): string {
if (c.daysLeft <= 0) return 'var(--km-g400)';
return c.daysLeft > 10 ? 'var(--km-amber)' : 'var(--km-red)';
}
responseClass(r: string): string {
const m: Record<string, string> = {
'No Action': 'd-pe',
'No Response': 'd-fa',
Pending: 'd-pe',
Refused: 'd-fa',
'Not Found at Address': 'd-fa',
Awaited: 'd-pe'
};
return m[r] || 'd-pe';
}
}