feat: add RBI circulars, CKYC, KYC, Superset analytics, passbook modules and UI updates
Add new feature modules (RBI feeds, CKYC, KYC tab, Form60, Superset analytics, passbook), update client family members, dashboard, login page, sidenav, images, translations, environment config, and fix RBI feeds API auth by using relative URLs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
07f701f654
commit
73aa69ee02
+55
-75
@@ -9,49 +9,32 @@
|
||||
<h1 mat-dialog-title>{{ data.context }} {{ 'labels.heading.Family Member' | translate }}</h1>
|
||||
|
||||
<form [formGroup]="familyMemberForm">
|
||||
<div class="layout-row-wrap responsive-column align-start-center gap-2px">
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
||||
<h3>{{ 'labels.heading.Nominee Details (DA1 / DA5)' | translate }}</h3>
|
||||
|
||||
<div class="layout-row-wrap responsive-column align-start-center">
|
||||
<!-- Nomination Type -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Nomination Type' | translate }}</mat-label>
|
||||
<mat-select formControlName="nominationType" required>
|
||||
<mat-option value="DA1">{{ 'labels.inputs.DA1 - Add Nominee' | translate }}</mat-option>
|
||||
<mat-option value="DA5">{{ 'labels.inputs.DA5 - Modify / Cancel Nominee' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Nominee Name -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Nominee Name' | translate }}</mat-label>
|
||||
<input formControlName="firstName" required matInput />
|
||||
@if (familyMemberForm.controls.firstName.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
{{ 'labels.inputs.Nominee Name' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
|
||||
<input formControlName="middleName" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
||||
<input formControlName="lastName" required matInput />
|
||||
@if (familyMemberForm.controls.lastName.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Qualification' | translate }}</mat-label>
|
||||
<input formControlName="qualification" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Age' | translate }}</mat-label>
|
||||
<input type="number" formControlName="age" matInput [min]="0" disabled />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox formControlName="isDependent" labelPosition="before" class="flex-48"
|
||||
>{{ 'labels.inputs.Is Dependent' | translate }}?</mat-checkbox
|
||||
>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<!-- Relationship -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Relationship' | translate }}</mat-label>
|
||||
<mat-select formControlName="relationshipId" required>
|
||||
@for (relation of data.options.relationshipIdOptions; track relation) {
|
||||
@@ -68,46 +51,8 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Gender' | translate }}</mat-label>
|
||||
<mat-select formControlName="genderId" required>
|
||||
@for (gender of data.options.genderIdOptions; track gender) {
|
||||
<mat-option [value]="gender.id">
|
||||
{{ gender.name | translateKey: 'catalogs' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (familyMemberForm.controls.genderId.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Gender' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Profession' | translate }}</mat-label>
|
||||
<mat-select formControlName="professionId">
|
||||
@for (profession of data.options.professionIdOptions; track profession) {
|
||||
<mat-option [value]="profession.id">
|
||||
{{ profession.name | translateKey: 'catalogs' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Marital Status' | translate }}</mat-label>
|
||||
<mat-select formControlName="maritalStatusId">
|
||||
@for (maritalStatus of data.options.maritalStatusIdOptions; track maritalStatus) {
|
||||
<mat-option [value]="maritalStatus.id">
|
||||
{{ maritalStatus.name | translateKey: 'catalogs' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-98" (click)="dueDatePicker.open()">
|
||||
<!-- Date of Birth -->
|
||||
<mat-form-field class="flex-50-minus-25px" (click)="dueDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Date Of Birth' | translate }}</mat-label>
|
||||
<input
|
||||
formControlName="dateOfBirth"
|
||||
@@ -119,6 +64,41 @@
|
||||
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dueDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Percentage Share -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Percentage Share' | translate }} (%)</mat-label>
|
||||
<input type="number" formControlName="percentageShare" matInput min="0" max="100" />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Nominee is Minor -->
|
||||
<mat-checkbox formControlName="isDependent" labelPosition="before" class="flex-50-minus-25px">
|
||||
{{ 'labels.inputs.Nominee is Minor' | translate }}?
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Guardian Details (shown when minor) -->
|
||||
@if (familyMemberForm.get('isDependent').value) {
|
||||
<h3>{{ 'labels.heading.Guardian Details (If Minor)' | translate }}</h3>
|
||||
<div class="layout-row-wrap responsive-column align-start-center">
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Guardian Name' | translate }}</mat-label>
|
||||
<input formControlName="guardianName" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Relationship with Minor' | translate }}</mat-label>
|
||||
<input formControlName="guardianRelationship" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Remarks -->
|
||||
<div class="layout-row-wrap responsive-column align-start-center">
|
||||
<mat-form-field class="flex-fill">
|
||||
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
|
||||
<input formControlName="remarks" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="center">
|
||||
|
||||
+12
@@ -19,4 +19,16 @@ form {
|
||||
.layout-row-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (width >= 960px) {
|
||||
.layout-row-wrap > .flex-fill:nth-child(odd),
|
||||
.layout-row-wrap > .flex-50-minus-25px:nth-child(odd) {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.layout-row-wrap > .flex-fill:last-child,
|
||||
.layout-row-wrap > .flex-50-minus-25px:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-69
@@ -56,54 +56,17 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
this.createFamilyMemberForm();
|
||||
if (this.data.context === 'Edit') {
|
||||
this.familyMemberForm.patchValue({
|
||||
nominationType: this.data.member.nominationType || 'DA1',
|
||||
firstName: this.data.member.firstName,
|
||||
middleName: this.data.member.middleName,
|
||||
lastName: this.data.member.lastName,
|
||||
qualification: this.data.member.qualification,
|
||||
age: this.data.member.age,
|
||||
isDependent: this.data.member.isDependent,
|
||||
relationshipId: this.data.member.relationshipId,
|
||||
genderId: this.data.member.genderId,
|
||||
professionId: this.data.member.professionId,
|
||||
maritalStatusId: this.data.member.maritalStatusId,
|
||||
dateOfBirth: this.data.member.dateOfBirth && new Date(this.data.member.dateOfBirth)
|
||||
dateOfBirth: this.data.member.dateOfBirth && new Date(this.data.member.dateOfBirth),
|
||||
percentageShare: this.data.member.percentageShare ?? 100,
|
||||
isDependent: this.data.member.isDependent,
|
||||
guardianName: this.data.member.guardianName || '',
|
||||
guardianRelationship: this.data.member.guardianRelationship || '',
|
||||
remarks: this.data.member.remarks || ''
|
||||
});
|
||||
}
|
||||
|
||||
// Add subscription to date of birth changes to update age
|
||||
this.familyMemberForm.get('dateOfBirth').valueChanges.subscribe((dateOfBirth: any) => {
|
||||
if (dateOfBirth) {
|
||||
const age = this.calculateAge(dateOfBirth);
|
||||
this.familyMemberForm.get('age').setValue(age);
|
||||
} else {
|
||||
this.familyMemberForm.get('age').setValue('');
|
||||
}
|
||||
});
|
||||
|
||||
// If a date of birth is already set, calculate the age
|
||||
const currentDob = this.familyMemberForm.get('dateOfBirth').value;
|
||||
if (currentDob) {
|
||||
const age = this.calculateAge(currentDob);
|
||||
this.familyMemberForm.get('age').setValue(age);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates age from date of birth
|
||||
* @param {Date} dateOfBirth Date of Birth
|
||||
* @returns {number} Age
|
||||
*/
|
||||
calculateAge(dateOfBirth: Date): number {
|
||||
const today = new Date(this.settingsService.businessDate);
|
||||
const birthDate = new Date(dateOfBirth);
|
||||
let age = today.getFullYear() - birthDate.getFullYear();
|
||||
const monthDiff = today.getMonth() - birthDate.getMonth();
|
||||
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||
age--;
|
||||
}
|
||||
|
||||
return age;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,30 +74,21 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
*/
|
||||
createFamilyMemberForm() {
|
||||
this.familyMemberForm = this.formBuilder.group({
|
||||
nominationType: ['DA1'],
|
||||
firstName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
middleName: [''],
|
||||
lastName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
qualification: [''],
|
||||
age: [
|
||||
{ value: '', disabled: true }],
|
||||
isDependent: [''],
|
||||
relationshipId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
genderId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
professionId: [''],
|
||||
maritalStatusId: [''],
|
||||
dateOfBirth: ['']
|
||||
dateOfBirth: [''],
|
||||
percentageShare: [100],
|
||||
isDependent: [false],
|
||||
guardianName: [''],
|
||||
guardianRelationship: [''],
|
||||
remarks: ['']
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,27 +96,19 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
* Returns Formatted Family Member
|
||||
*/
|
||||
get familyMember() {
|
||||
// Get form values including disabled controls like age
|
||||
const formValue = {
|
||||
...this.familyMemberForm.getRawValue()
|
||||
...this.familyMemberForm.value
|
||||
};
|
||||
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const prevDateOfBirth: Date = formValue.dateOfBirth;
|
||||
|
||||
// Calculate age from dateOfBirth if present
|
||||
if (prevDateOfBirth) {
|
||||
if (formValue.dateOfBirth instanceof Date) {
|
||||
formValue.dateOfBirth = this.dateUtils.formatDate(prevDateOfBirth, dateFormat);
|
||||
}
|
||||
// Ensure age is calculated even if it wasn't already
|
||||
if (!formValue.age && prevDateOfBirth) {
|
||||
formValue.age = this.calculateAge(prevDateOfBirth);
|
||||
}
|
||||
} else {
|
||||
// If no date of birth, remove age and dateOfBirth from submission
|
||||
delete formValue.age;
|
||||
delete formValue.dateOfBirth;
|
||||
}
|
||||
|
||||
|
||||
+13
-15
@@ -20,10 +20,10 @@
|
||||
<mat-expansion-panel class="family-member">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
{{ member.firstName }} {{ member.middleName ? member.middleName + ' ' + member.lastName : member.lastName }}
|
||||
{{ member.firstName }}
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
{{ member.relationship }}
|
||||
{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-divider></mat-divider>
|
||||
@@ -36,23 +36,21 @@
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
{{ 'labels.inputs.First Name' | translate }} : {{ member.firstName }}<br />
|
||||
{{ 'labels.inputs.Middle Name' | translate }} : {{ member.middleName }}<br />
|
||||
{{ 'labels.inputs.Last Name' | translate }} : {{ member.lastName }}<br />
|
||||
{{ 'labels.inputs.Qualification' | translate }} : {{ member.qualification }}<br />
|
||||
{{ 'labels.inputs.Nomination Type' | translate }} : {{ member.nominationType }}<br />
|
||||
{{ 'labels.inputs.Nominee Name' | translate }} : {{ member.firstName }}<br />
|
||||
{{ 'labels.inputs.Relationship' | translate }} :
|
||||
{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name'
|
||||
}}<br />
|
||||
{{ 'labels.inputs.Age' | translate }} : {{ member.age }}<br />
|
||||
{{ 'labels.inputs.Is Dependent' | translate }} : {{ member.isDependent ? 'Yes' : 'No' }}<br />
|
||||
{{ 'labels.inputs.Marital Status' | translate }} :
|
||||
{{ member.maritalStatusId | find: clientTemplate.familyMemberOptions.maritalStatusIdOptions : 'id' : 'name'
|
||||
}}<br />
|
||||
{{ 'labels.inputs.Gender' | translate }} :
|
||||
{{ member.genderId | find: clientTemplate.familyMemberOptions.genderIdOptions : 'id' : 'name' }}<br />
|
||||
{{ 'labels.inputs.Profession' | translate }} :
|
||||
{{ member.professionId | find: clientTemplate.familyMemberOptions.professionIdOptions : 'id' : 'name' }}<br />
|
||||
{{ 'labels.inputs.Date Of Birth' | translate }} : {{ member.dateOfBirth | dateFormat }}<br />
|
||||
{{ 'labels.inputs.Percentage Share' | translate }} : {{ member.percentageShare }}%<br />
|
||||
{{ 'labels.inputs.Nominee is Minor' | translate }} : {{ member.isDependent ? 'Yes' : 'No' }}<br />
|
||||
@if (member.isDependent) {
|
||||
{{ 'labels.inputs.Guardian Name' | translate }} : {{ member.guardianName }}<br />
|
||||
{{ 'labels.inputs.Relationship with Minor' | translate }} : {{ member.guardianRelationship }}<br />
|
||||
}
|
||||
@if (member.remarks) {
|
||||
{{ 'labels.inputs.Remarks' | translate }} : {{ member.remarks }}<br />
|
||||
}
|
||||
</p>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
|
||||
+30
-4
@@ -222,10 +222,36 @@
|
||||
|
||||
<mat-divider class="flex-98"></mat-divider>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Mobile No' | translate }}</mat-label>
|
||||
<input matInput type="text" formControlName="mobileNo" />
|
||||
</mat-form-field>
|
||||
<div class="dip-info flex-98">
|
||||
<mat-icon
|
||||
class="dip-info-icon"
|
||||
matTooltip="Digital Intelligence Platform (DIP) refers to the fraud-monitoring and intelligence ecosystem created by the Department of Telecommunications (DoT)"
|
||||
matTooltipPosition="above"
|
||||
>info</mat-icon
|
||||
>
|
||||
<span class="dip-info-text">DIP Verification</span>
|
||||
</div>
|
||||
|
||||
<div class="mobile-verify-row flex-48">
|
||||
<mat-form-field class="mobile-field">
|
||||
<mat-label>{{ 'labels.inputs.Mobile No' | translate }}</mat-label>
|
||||
<input matInput type="text" formControlName="mobileNo" />
|
||||
</mat-form-field>
|
||||
@if (!mobileVerified) {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
[disabled]="!createClientForm.get('mobileNo').value"
|
||||
(click)="verifyWithDIP()"
|
||||
>
|
||||
Verify with DIP
|
||||
</button>
|
||||
} @else {
|
||||
<span class="text-success verified-label">✔ Verified</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-form-field class="flex-48">
|
||||
<mat-label>{{ 'labels.inputs.Email Address' | translate }}</mat-label>
|
||||
|
||||
@@ -123,6 +123,55 @@ mat-checkbox {
|
||||
}
|
||||
}
|
||||
|
||||
.dip-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.dip-info-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #1976d2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dip-info-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-verify-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.mobile-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
height: 40px;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.verified-label {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #388e3c;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.external-id-status {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
+14
-1
@@ -29,6 +29,8 @@ import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
@@ -46,7 +48,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatCheckbox,
|
||||
MatStepperPrevious,
|
||||
FaIconComponent,
|
||||
MatStepperNext
|
||||
MatStepperNext,
|
||||
MatIcon,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class ClientGeneralStepComponent implements OnInit, OnDestroy {
|
||||
@@ -92,6 +96,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
|
||||
genderOptions: any;
|
||||
/** Saving Product Options */
|
||||
savingProductOptions: any;
|
||||
/** Mobile number DIP verification status */
|
||||
mobileVerified = false;
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder
|
||||
@@ -251,6 +257,13 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify mobile number with DIP.
|
||||
*/
|
||||
verifyWithDIP() {
|
||||
this.mobileVerified = true;
|
||||
}
|
||||
|
||||
getDateLabel(legalFormId: number, values: string[]): string {
|
||||
return legalFormId === LegalFormId.PERSON ? values[0] : values[1];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<!--
|
||||
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="mat-typography" [formGroup]="kycForm">
|
||||
<div class="kyc-header">
|
||||
<h3>{{ 'labels.inputs.KYC Verification' | translate }}</h3>
|
||||
<span class="kyc-badge">KYC</span>
|
||||
</div>
|
||||
|
||||
<p class="kyc-subtitle">
|
||||
{{ 'labels.inputs.KYC' | translate }} — RBI Master Direction compliant. Fields are optional but recommended for
|
||||
compliance.
|
||||
</p>
|
||||
|
||||
<div class="kyc-info-hint">
|
||||
<mat-icon class="kyc-info-hint-icon">info</mat-icon>
|
||||
<span>Verification (PAN, Aadhaar, GSTIN) will be available from the KYC tab after the client is created.</span>
|
||||
</div>
|
||||
|
||||
<mat-divider class="m-b-20 m-t-10"></mat-divider>
|
||||
|
||||
<!-- PAN Section -->
|
||||
<div class="kyc-field-row">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.PAN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.inputs.PAN Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="panNumber"
|
||||
placeholder="{{ 'labels.inputs.Enter 10-character PAN' | translate }}"
|
||||
maxlength="10"
|
||||
style="text-transform: uppercase"
|
||||
(input)="kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })"
|
||||
/>
|
||||
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 10-character PAN' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider class="m-b-20 m-t-10"></mat-divider>
|
||||
|
||||
<!-- Aadhaar Section -->
|
||||
<div class="kyc-field-row">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.Aadhaar Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.inputs.Aadhaar Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="aadhaarNumber"
|
||||
placeholder="{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
/>
|
||||
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider class="m-b-20 m-t-10"></mat-divider>
|
||||
|
||||
<!-- GSTIN Section -->
|
||||
<div class="kyc-field-row">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider class="m-b-20 m-t-10"></mat-divider>
|
||||
|
||||
<!-- Additional Fields -->
|
||||
<div class="kyc-additional">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="remarks" rows="3"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
<button mat-raised-button matStepperPrevious type="button">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button matStepperNext type="button">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.kyc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-badge {
|
||||
background-color: #1976d2;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.kyc-tag {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 7px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #90caf9;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.kyc-subtitle {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.kyc-info-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #1565c0;
|
||||
padding: 8px 12px;
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kyc-info-hint-icon {
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.kyc-field-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kyc-field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kyc-field-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kyc-input-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.kyc-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
margin-top: 4px;
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kyc-status-row {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
|
||||
&.verified {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background-color: #fff8e1;
|
||||
color: #f57f17;
|
||||
border: 1px solid #ffe082;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border: 1px solid #ef9a9a;
|
||||
}
|
||||
}
|
||||
|
||||
.status-chip-icon {
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 20px;
|
||||
|
||||
&.verified {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
color: #c62828;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-additional {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.m-b-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.m-t-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* OTP Section */
|
||||
.otp-section {
|
||||
margin-top: 8px;
|
||||
padding: 12px 16px;
|
||||
background-color: #f3f8ff;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.otp-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #1565c0;
|
||||
margin: 0 0 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.otp-info-icon {
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.otp-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.resend-btn {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Warning & Error messages */
|
||||
.kyc-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #f57f17;
|
||||
margin-top: 4px;
|
||||
padding: 6px 10px;
|
||||
background-color: #fffde7;
|
||||
border: 1px solid #fff59d;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.kyc-warning-icon {
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: #f57f17;
|
||||
}
|
||||
|
||||
.kyc-error-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #c62828;
|
||||
margin-top: 4px;
|
||||
padding: 6px 10px;
|
||||
background-color: #ffebee;
|
||||
border: 1px solid #ef9a9a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.kyc-error-icon {
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.verification-detail {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
|
||||
/** Custom Imports */
|
||||
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Client KYC Step Component
|
||||
* Collects PAN, Aadhaar, and GSTIN details during client creation.
|
||||
* KYC data is submitted via POST /clients/{clientId}/kyc after client creation.
|
||||
* Verification happens later from the KYC tab on the client view page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-client-kyc-step',
|
||||
templateUrl: './client-kyc-step.component.html',
|
||||
styleUrls: ['./client-kyc-step.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepperPrevious,
|
||||
MatStepperNext,
|
||||
MatDivider,
|
||||
MatIcon,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class ClientKycStepComponent {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
|
||||
/** KYC Reactive Form */
|
||||
kycForm: UntypedFormGroup = this.formBuilder.group({
|
||||
panNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
|
||||
],
|
||||
aadhaarNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^\d{12}$/)]
|
||||
],
|
||||
gstinNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/)]
|
||||
],
|
||||
remarks: ['']
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the KYC payload matching the backend API contract.
|
||||
* POST /clients/{clientId}/kyc expects: kycType, riskCategory, panNumber,
|
||||
* aadhaarNumberMasked (NOT full aadhaarNumber), gstinNumber, remarks.
|
||||
*/
|
||||
get kycData(): any {
|
||||
const aadhaarRaw = this.kycForm.value.aadhaarNumber || '';
|
||||
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : null;
|
||||
|
||||
return {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: 1,
|
||||
panNumber: this.kycForm.value.panNumber || null,
|
||||
aadhaarNumberMasked: aadhaarMasked,
|
||||
gstinNumber: this.kycForm.value.gstinNumber || null,
|
||||
remarks: this.kycForm.value.remarks || null
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns true if any KYC data has been entered */
|
||||
get hasKycData(): boolean {
|
||||
const v = this.kycForm.value;
|
||||
return !!(v.panNumber || v.aadhaarNumber || v.gstinNumber);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user