diff --git a/angular.json b/angular.json index 323a10ada..1920fbe55 100644 --- a/angular.json +++ b/angular.json @@ -105,7 +105,7 @@ "builder": "@angular/build:dev-server", "options": { "buildTarget": "mifosx-web-app:build", - "proxyConfig": "proxy.conf.js" + "proxyConfig": "proxy.localhost.conf.js" }, "configurations": { "development": { diff --git a/package.json b/package.json index 9372d57bf..11771d707 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "ng": "ng", "build": "node version.js && npm run env -s && ng build --configuration production --output-hashing=none", "build:prod": "node version.js && node --max-old-space-size=16384 ./node_modules/@angular/cli/bin/ng build --configuration production --output-hashing=none --base-href=/web-app/", - "start": "npm run env -s && ng serve --proxy-config proxy.conf.js", - "serve:dev": "npm run env -s && ng serve --source-map", + "start": "npm run env -s && ng serve --proxy-config proxy.localhost.conf.js", + "start:demo": "npm run env -s && ng serve --proxy-config proxy.conf.js", + "serve:dev": "npm run env -s && ng serve --source-map --proxy-config proxy.localhost.conf.js", "serve:sw": "npm run build -s && npx http-server ./dist -p 4200", "lint": "eslint . && stylelint \"src/**/*.scss\" && prettier . --check && htmlhint \"src\" --config .htmlhintrc", "test": "jest --config jest.config.ts", diff --git a/proxy.conf.js b/proxy.conf.js index dda6cde79..e5f9b19a6 100644 --- a/proxy.conf.js +++ b/proxy.conf.js @@ -20,13 +20,12 @@ const proxyConfig = [ { context: ['/fineract-provider'], target: 'https://demo.mifos.community', - pathRewrite: { '^/fineract-provider': '' }, changeOrigin: true, secure: true, logLevel: 'debug', onProxyReq: function (proxyReq, req, res) { - const rewrittenPath = (req.url || '').replace(/^\/fineract-provider/, ''); - console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath); + const forwardedPath = req.url || ''; + console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + forwardedPath); }, onError: function (err, req, res) { console.error( diff --git a/proxy.localhost.conf.js b/proxy.localhost.conf.js index 88d5d190f..a3cb77478 100644 --- a/proxy.localhost.conf.js +++ b/proxy.localhost.conf.js @@ -16,14 +16,13 @@ module.exports = [ { context: ['/fineract-provider'], - target: 'http://localhost:8443', - pathRewrite: { '^/fineract-provider': '' }, + target: 'https://localhost:8443', changeOrigin: true, secure: false, logLevel: 'debug', onProxyReq: function (proxyReq, req, res) { - const rewrittenPath = (req.url || '').replace(/^\/fineract-provider/, ''); - console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + rewrittenPath); + const forwardedPath = req.url || ''; + console.log('[Proxy] Proxying:', req.method, req.url, '->', this.target + forwardedPath); }, onError: function (err, req, res) { console.error( diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cd95d8e33..75d6543a7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -49,6 +49,9 @@ import { ProfileModule } from './profile/profile.module'; import { TasksModule } from './tasks/tasks.module'; import { ConfigurationWizardModule } from './configuration-wizard/configuration-wizard.module'; import { RemittancesModule } from './remittances/remittances.module'; +import { SupersetAnalyticsModule } from './superset-analytics/superset-analytics.module'; +import { CkycModule } from './ckyc/ckyc.module'; +import { RbiCircularsModule } from './rbi-circulars/rbi-circulars.module'; import { PortalModule } from '@angular/cdk/portal'; /** Main Routing Module */ @@ -129,6 +132,9 @@ export function HttpLoaderFactory(http: HttpClient) { TasksModule, ConfigurationWizardModule, RemittancesModule, + SupersetAnalyticsModule, + CkycModule, + RbiCircularsModule, AppRoutingModule, NotFoundComponent, CallbackComponent, diff --git a/src/app/ckyc/ckyc-reports/ckyc-reports.component.html b/src/app/ckyc/ckyc-reports/ckyc-reports.component.html new file mode 100644 index 000000000..f90b22d87 --- /dev/null +++ b/src/app/ckyc/ckyc-reports/ckyc-reports.component.html @@ -0,0 +1,19 @@ + + +
+ + +
+ +

{{ 'labels.heading.CKYC Reports' | translate }}

+

{{ 'labels.text.This section is under development.' | translate }}

+
+
+
+
diff --git a/src/app/ckyc/ckyc-reports/ckyc-reports.component.ts b/src/app/ckyc/ckyc-reports/ckyc-reports.component.ts new file mode 100644 index 000000000..ad073202c --- /dev/null +++ b/src/app/ckyc/ckyc-reports/ckyc-reports.component.ts @@ -0,0 +1,21 @@ +/** + * 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'mifosx-ckyc-reports', + templateUrl: './ckyc-reports.component.html', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class CkycReportsComponent {} diff --git a/src/app/ckyc/ckyc-routing.module.ts b/src/app/ckyc/ckyc-routing.module.ts new file mode 100644 index 000000000..f8aff8a02 --- /dev/null +++ b/src/app/ckyc/ckyc-routing.module.ts @@ -0,0 +1,58 @@ +/** + * 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { Route } from '../core/route/route.service'; +import { CkycComponent } from './ckyc.component'; +import { RecordMasterComponent } from './record-master/record-master.component'; +import { CkycVerificationComponent } from './ckyc-verification/ckyc-verification.component'; +import { CkycReportsComponent } from './ckyc-reports/ckyc-reports.component'; +import { EkycComponent } from './ekyc/ekyc.component'; + +const routes: Routes = [ + Route.withShell([ + { + path: 'ckyc', + data: { title: 'CKYC', breadcrumb: 'CKYC' }, + children: [ + { + path: '', + component: CkycComponent + }, + { + path: 'record-master', + component: RecordMasterComponent, + data: { title: 'Record Master', breadcrumb: 'Record Master' } + }, + { + path: 'ckyc-verification', + component: CkycVerificationComponent, + data: { title: 'CKYC', breadcrumb: 'CKYC Verification' } + }, + { + path: 'ckyc-reports', + component: CkycReportsComponent, + data: { title: 'CKYC Reports', breadcrumb: 'CKYC Reports' } + }, + { + path: 'ekyc', + component: EkycComponent, + data: { title: 'E-KYC', breadcrumb: 'E-KYC' } + } + ] + } + ]) +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class CkycRoutingModule {} diff --git a/src/app/ckyc/ckyc-verification/ckyc-verification.component.html b/src/app/ckyc/ckyc-verification/ckyc-verification.component.html new file mode 100644 index 000000000..604611c58 --- /dev/null +++ b/src/app/ckyc/ckyc-verification/ckyc-verification.component.html @@ -0,0 +1,19 @@ + + +
+ + +
+ +

{{ 'labels.heading.CKYC' | translate }}

+

{{ 'labels.text.This section is under development.' | translate }}

+
+
+
+
diff --git a/src/app/ckyc/ckyc-verification/ckyc-verification.component.ts b/src/app/ckyc/ckyc-verification/ckyc-verification.component.ts new file mode 100644 index 000000000..8d62b9d5c --- /dev/null +++ b/src/app/ckyc/ckyc-verification/ckyc-verification.component.ts @@ -0,0 +1,21 @@ +/** + * 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'mifosx-ckyc-verification', + templateUrl: './ckyc-verification.component.html', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class CkycVerificationComponent {} diff --git a/src/app/ckyc/ckyc.component.html b/src/app/ckyc/ckyc.component.html new file mode 100644 index 000000000..597ecb322 --- /dev/null +++ b/src/app/ckyc/ckyc.component.html @@ -0,0 +1,125 @@ + + +
+ +
+
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+
+
+
diff --git a/src/app/ckyc/ckyc.component.scss b/src/app/ckyc/ckyc.component.scss new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ b/src/app/ckyc/ckyc.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/ckyc/ckyc.component.ts b/src/app/ckyc/ckyc.component.ts new file mode 100644 index 000000000..315569c26 --- /dev/null +++ b/src/app/ckyc/ckyc.component.ts @@ -0,0 +1,36 @@ +/** + * 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 { RouterLink } from '@angular/router'; +import { MatNavList, MatListItem } from '@angular/material/list'; +import { MatIcon } from '@angular/material/icon'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatLine } from '@angular/material/grid-list'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-ckyc', + templateUrl: './ckyc.component.html', + styleUrls: ['./ckyc.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatNavList, + MatListItem, + MatIcon, + FaIconComponent, + MatLine + ] +}) +export class CkycComponent { + arrowBooleans: boolean[] = new Array(4).fill(false); + + arrowBooleansToggle(arrowNumber: number) { + this.arrowBooleans[arrowNumber] = !this.arrowBooleans[arrowNumber]; + } +} diff --git a/src/app/ckyc/ckyc.module.ts b/src/app/ckyc/ckyc.module.ts new file mode 100644 index 000000000..4d6a0b6ad --- /dev/null +++ b/src/app/ckyc/ckyc.module.ts @@ -0,0 +1,30 @@ +/** + * 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 { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { CkycRoutingModule } from './ckyc-routing.module'; +import { CkycComponent } from './ckyc.component'; +import { RecordMasterComponent } from './record-master/record-master.component'; +import { CkycVerificationComponent } from './ckyc-verification/ckyc-verification.component'; +import { CkycReportsComponent } from './ckyc-reports/ckyc-reports.component'; +import { EkycComponent } from './ekyc/ekyc.component'; + +@NgModule({ + imports: [ + SharedModule, + CkycRoutingModule, + CkycComponent, + RecordMasterComponent, + CkycVerificationComponent, + CkycReportsComponent, + EkycComponent + ] +}) +export class CkycModule {} diff --git a/src/app/ckyc/ekyc/ekyc.component.html b/src/app/ckyc/ekyc/ekyc.component.html new file mode 100644 index 000000000..1345e177b --- /dev/null +++ b/src/app/ckyc/ekyc/ekyc.component.html @@ -0,0 +1,19 @@ + + +
+ + +
+ +

{{ 'labels.heading.E-KYC' | translate }}

+

{{ 'labels.text.This section is under development.' | translate }}

+
+
+
+
diff --git a/src/app/ckyc/ekyc/ekyc.component.ts b/src/app/ckyc/ekyc/ekyc.component.ts new file mode 100644 index 000000000..59732d290 --- /dev/null +++ b/src/app/ckyc/ekyc/ekyc.component.ts @@ -0,0 +1,21 @@ +/** + * 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'mifosx-ekyc', + templateUrl: './ekyc.component.html', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class EkycComponent {} diff --git a/src/app/ckyc/record-master/record-master.component.html b/src/app/ckyc/record-master/record-master.component.html new file mode 100644 index 000000000..c7ad688fc --- /dev/null +++ b/src/app/ckyc/record-master/record-master.component.html @@ -0,0 +1,19 @@ + + +
+ + +
+ +

{{ 'labels.heading.Record Master' | translate }}

+

{{ 'labels.text.This section is under development.' | translate }}

+
+
+
+
diff --git a/src/app/ckyc/record-master/record-master.component.ts b/src/app/ckyc/record-master/record-master.component.ts new file mode 100644 index 000000000..cb0e10aa8 --- /dev/null +++ b/src/app/ckyc/record-master/record-master.component.ts @@ -0,0 +1,21 @@ +/** + * 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'mifosx-record-master', + templateUrl: './record-master.component.html', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class RecordMasterComponent {} diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html index 65f7f6a53..416c1b7d1 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html @@ -9,49 +9,32 @@

{{ data.context }} {{ 'labels.heading.Family Member' | translate }}

-
- - {{ 'labels.inputs.First Name' | translate }} +

{{ 'labels.heading.Nominee Details (DA1 / DA5)' | translate }}

+ +
+ + + {{ 'labels.inputs.Nomination Type' | translate }} + + {{ 'labels.inputs.DA1 - Add Nominee' | translate }} + {{ 'labels.inputs.DA5 - Modify / Cancel Nominee' | translate }} + + + + + + {{ 'labels.inputs.Nominee Name' | translate }} @if (familyMemberForm.controls.firstName.hasError('required')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Nominee Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} } - - {{ 'labels.inputs.Middle Name' | translate }} - - - - - {{ 'labels.inputs.Last Name' | translate }} - - @if (familyMemberForm.controls.lastName.hasError('required')) { - - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Qualification' | translate }} - - - - - {{ 'labels.inputs.Age' | translate }} - - - - {{ 'labels.inputs.Is Dependent' | translate }}? - - + + {{ 'labels.inputs.Relationship' | translate }} @for (relation of data.options.relationshipIdOptions; track relation) { @@ -68,46 +51,8 @@ } - - {{ 'labels.inputs.Gender' | translate }} - - @for (gender of data.options.genderIdOptions; track gender) { - - {{ gender.name | translateKey: 'catalogs' }} - - } - - @if (familyMemberForm.controls.genderId.hasError('required')) { - - {{ 'labels.inputs.Gender' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Profession' | translate }} - - @for (profession of data.options.professionIdOptions; track profession) { - - {{ profession.name | translateKey: 'catalogs' }} - - } - - - - - {{ 'labels.inputs.Marital Status' | translate }} - - @for (maritalStatus of data.options.maritalStatusIdOptions; track maritalStatus) { - - {{ maritalStatus.name | translateKey: 'catalogs' }} - - } - - - - + + {{ 'labels.inputs.Date Of Birth' | translate }} + + + + {{ 'labels.inputs.Percentage Share' | translate }} (%) + + + + + + {{ 'labels.inputs.Nominee is Minor' | translate }}? + +
+ + + @if (familyMemberForm.get('isDependent').value) { +

{{ 'labels.heading.Guardian Details (If Minor)' | translate }}

+
+ + {{ 'labels.inputs.Guardian Name' | translate }} + + + + + {{ 'labels.inputs.Relationship with Minor' | translate }} + + +
+ } + + +
+ + {{ 'labels.inputs.Remarks' | translate }} + +
diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss index 91b72dab6..ff3254c01 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss @@ -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; + } + } } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts index fedf3c57a..df589a286 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts @@ -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; } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html index af10c6748..0cfb46e48 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html @@ -20,10 +20,10 @@ - {{ member.firstName }} {{ member.middleName ? member.middleName + ' ' + member.lastName : member.lastName }} + {{ member.firstName }} - {{ member.relationship }} + {{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }} @@ -36,23 +36,21 @@

- {{ 'labels.inputs.First Name' | translate }} : {{ member.firstName }}
- {{ 'labels.inputs.Middle Name' | translate }} : {{ member.middleName }}
- {{ 'labels.inputs.Last Name' | translate }} : {{ member.lastName }}
- {{ 'labels.inputs.Qualification' | translate }} : {{ member.qualification }}
+ {{ 'labels.inputs.Nomination Type' | translate }} : {{ member.nominationType }}
+ {{ 'labels.inputs.Nominee Name' | translate }} : {{ member.firstName }}
{{ 'labels.inputs.Relationship' | translate }} : {{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }}
- {{ 'labels.inputs.Age' | translate }} : {{ member.age }}
- {{ 'labels.inputs.Is Dependent' | translate }} : {{ member.isDependent ? 'Yes' : 'No' }}
- {{ 'labels.inputs.Marital Status' | translate }} : - {{ member.maritalStatusId | find: clientTemplate.familyMemberOptions.maritalStatusIdOptions : 'id' : 'name' - }}
- {{ 'labels.inputs.Gender' | translate }} : - {{ member.genderId | find: clientTemplate.familyMemberOptions.genderIdOptions : 'id' : 'name' }}
- {{ 'labels.inputs.Profession' | translate }} : - {{ member.professionId | find: clientTemplate.familyMemberOptions.professionIdOptions : 'id' : 'name' }}
{{ 'labels.inputs.Date Of Birth' | translate }} : {{ member.dateOfBirth | dateFormat }}
+ {{ 'labels.inputs.Percentage Share' | translate }} : {{ member.percentageShare }}%
+ {{ 'labels.inputs.Nominee is Minor' | translate }} : {{ member.isDependent ? 'Yes' : 'No' }}
+ @if (member.isDependent) { + {{ 'labels.inputs.Guardian Name' | translate }} : {{ member.guardianName }}
+ {{ 'labels.inputs.Relationship with Minor' | translate }} : {{ member.guardianRelationship }}
+ } + @if (member.remarks) { + {{ 'labels.inputs.Remarks' | translate }} : {{ member.remarks }}
+ }

} diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html index ba69c7b13..7f60c80af 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html @@ -222,10 +222,36 @@ - - {{ 'labels.inputs.Mobile No' | translate }} - - +
+ info + DIP Verification +
+ +
+ + {{ 'labels.inputs.Mobile No' | translate }} + + + @if (!mobileVerified) { + + } @else { + ✔ Verified + } +
{{ 'labels.inputs.Email Address' | translate }} diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss b/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss index c70f269b7..42a67d85c 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.scss @@ -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; diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index d607e414c..4295b9203 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -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]; } diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html new file mode 100644 index 000000000..ceed0902e --- /dev/null +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html @@ -0,0 +1,117 @@ + + +
+
+

{{ 'labels.inputs.KYC Verification' | translate }}

+ KYC +
+ +

+ {{ 'labels.inputs.KYC' | translate }} — RBI Master Direction compliant. Fields are optional but recommended for + compliance. +

+ +
+ info + Verification (PAN, Aadhaar, GSTIN) will be available from the KYC tab after the client is created. +
+ + + + +
+
+ {{ 'labels.inputs.PAN Number' | translate }} + KYC +
+ + {{ 'labels.inputs.PAN Number' | translate }} + + @if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) { + {{ 'labels.inputs.Enter 10-character PAN' | translate }} + } + +
+ + + + +
+
+ {{ 'labels.inputs.Aadhaar Number' | translate }} + KYC +
+ + {{ 'labels.inputs.Aadhaar Number' | translate }} + + @if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) { + {{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }} + } + +
+ + + + +
+
+ {{ 'labels.inputs.GSTIN Number' | translate }} + KYC +
+ + {{ 'labels.inputs.GSTIN Number' | translate }} + + @if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) { + {{ 'labels.inputs.Enter 15-character GSTIN' | translate }} + } + +
+ + + + +
+ + {{ 'labels.inputs.Remarks' | translate }} + + +
+ + +
+ + +
+
diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss new file mode 100644 index 000000000..400792bc6 --- /dev/null +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss @@ -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; +} diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts new file mode 100644 index 000000000..cfe37e4b6 --- /dev/null +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts @@ -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); + } +} diff --git a/src/app/clients/clients-routing.module.ts b/src/app/clients/clients-routing.module.ts index 27c754589..d41b97718 100644 --- a/src/app/clients/clients-routing.module.ts +++ b/src/app/clients/clients-routing.module.ts @@ -55,6 +55,9 @@ import { ClientChargeViewResolver } from './common-resolvers/client-charge-view. import { ClientTransactionPayResolver } from './common-resolvers/client-transaction-pay.resolver'; import { ClientDataAndTemplateResolver } from './common-resolvers/client-and-template.resolver'; import { ClientCollateralResolver } from './common-resolvers/client-collateral.resolver'; +import { KycTabComponent } from './clients-view/kyc-tab/kyc-tab.component'; +import { CkycTabComponent } from './clients-view/ckyc-tab/ckyc-tab.component'; +import { Form60TabComponent } from './clients-view/form60-tab/form60-tab.component'; const routes: Routes = [ Route.withShell([ @@ -117,7 +120,7 @@ const routes: Routes = [ }, { path: 'family-members', - data: { title: 'Family Members', breadcrumb: 'Family Members', routeParamBreadcrumb: false }, + data: { title: 'Nominee', breadcrumb: 'Nominee', routeParamBreadcrumb: false }, children: [ { path: '', @@ -140,7 +143,7 @@ const routes: Routes = [ { path: 'edit', component: EditFamilyMemberComponent, - data: { title: 'Family Member View', routeParamBreadcrumb: 'familyMemberId' }, + data: { title: 'Nominee View', routeParamBreadcrumb: 'familyMemberId' }, resolve: { clientTemplate: ClientTemplateResolver, editFamilyMember: ClientFamilyMemberResolver @@ -159,6 +162,21 @@ const routes: Routes = [ clientIdentifierTemplate: ClientIdentifierTemplateResolver } }, + { + path: 'kyc', + component: KycTabComponent, + data: { title: 'KYC', breadcrumb: 'KYC', routeParamBreadcrumb: false } + }, + { + path: 'ckyc', + component: CkycTabComponent, + data: { title: 'CKYC', breadcrumb: 'CKYC', routeParamBreadcrumb: false } + }, + { + path: 'form60', + component: Form60TabComponent, + data: { title: 'Form 60', breadcrumb: 'Form 60', routeParamBreadcrumb: false } + }, { path: 'documents', component: DocumentsTabComponent, diff --git a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html new file mode 100644 index 000000000..813399750 --- /dev/null +++ b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html @@ -0,0 +1,351 @@ + + +
+ + @if (loading) { +
+ +

{{ 'labels.inputs.CKYC' | translate }}...

+
+ } + + @if (!loading) { + +
+
+

{{ 'labels.heading.CKYC Verification' | translate }}

+ CKYC +
+
+ + + + +
+ +
+ {{ 'labels.inputs.Account Number' | translate }} + + + @for (account of allAccounts; track account.accountNo) { + + {{ account.accountNo }} ({{ account.productName }}) + + } + + +
+ + +
+ {{ 'labels.inputs.Client ID' | translate }} + {{ clientId }} +
+ + +
+ {{ 'labels.inputs.Client Name' | translate }} + {{ clientName }} +
+ + +
+ {{ 'labels.inputs.Account Type' | translate }} + {{ selectedAccountType || '—' }} +
+
+
+
+ + +

{{ 'labels.heading.CKYC Documents' | translate }}

+ +
+ + + +
+ home + {{ 'labels.inputs.Address Proof Document' | translate }} +
+

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF

+
+ + + + + +
+ + @if (uploadErrors['ADDRESS_PROOF']) { +
+ error_outline + {{ uploadErrors['ADDRESS_PROOF'] }} +
+ } + @if (uploadResponses['ADDRESS_PROOF']) { +
+

+ {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['ADDRESS_PROOF'].status }} +

+

+ {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['ADDRESS_PROOF'].requestId }} +

+

+ {{ 'labels.inputs.Acknowledgement Number' | translate }}: + {{ uploadResponses['ADDRESS_PROOF'].acknowledgementNumber }} +

+

+ {{ 'labels.inputs.Processing Status' | translate }}: + {{ uploadResponses['ADDRESS_PROOF'].processingStatus }} +

+
+ } +
+
+ + + + +
+ photo_camera + {{ 'labels.inputs.Photo Proof Document' | translate }} +
+

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF

+
+ + + + + +
+ + @if (uploadErrors['PHOTO_PROOF']) { +
+ error_outline + {{ uploadErrors['PHOTO_PROOF'] }} +
+ } + @if (uploadResponses['PHOTO_PROOF']) { +
+

+ {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['PHOTO_PROOF'].status }} +

+

+ {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['PHOTO_PROOF'].requestId }} +

+

+ {{ 'labels.inputs.Acknowledgement Number' | translate }}: + {{ uploadResponses['PHOTO_PROOF'].acknowledgementNumber }} +

+

+ {{ 'labels.inputs.Processing Status' | translate }}: + {{ uploadResponses['PHOTO_PROOF'].processingStatus }} +

+
+ } +
+
+ + + + +
+ person + {{ 'labels.inputs.Image of Client' | translate }} +
+

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG

+
+ + + + + +
+ + @if (uploadErrors['CLIENT_IMAGE']) { +
+ error_outline + {{ uploadErrors['CLIENT_IMAGE'] }} +
+ } + @if (uploadResponses['CLIENT_IMAGE']) { +
+

+ {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['CLIENT_IMAGE'].status }} +

+

+ {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['CLIENT_IMAGE'].requestId }} +

+

+ {{ 'labels.inputs.Acknowledgement Number' | translate }}: + {{ uploadResponses['CLIENT_IMAGE'].acknowledgementNumber }} +

+

+ {{ 'labels.inputs.Processing Status' | translate }}: + {{ uploadResponses['CLIENT_IMAGE'].processingStatus }} +

+
+ } +
+
+ + + + +
+ draw + {{ 'labels.inputs.Signature of Client' | translate }} +
+

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG

+
+ + + + + +
+ + @if (uploadErrors['CLIENT_SIGNATURE']) { +
+ error_outline + {{ uploadErrors['CLIENT_SIGNATURE'] }} +
+ } + @if (uploadResponses['CLIENT_SIGNATURE']) { +
+

+ {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['CLIENT_SIGNATURE'].status }} +

+

+ {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['CLIENT_SIGNATURE'].requestId }} +

+

+ {{ 'labels.inputs.Acknowledgement Number' | translate }}: + {{ uploadResponses['CLIENT_SIGNATURE'].acknowledgementNumber }} +

+

+ {{ 'labels.inputs.Processing Status' | translate }}: + {{ uploadResponses['CLIENT_SIGNATURE'].processingStatus }} +

+
+ } +
+
+
+ } +
diff --git a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss new file mode 100644 index 000000000..1b5d6650d --- /dev/null +++ b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss @@ -0,0 +1,182 @@ +/** + * 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/. + */ + +.ckyc-tab-container { + padding: 16px; +} + +.ckyc-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + gap: 16px; + color: #666; +} + +.ckyc-tab-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.ckyc-tab-title { + display: flex; + align-items: center; + gap: 10px; + + h3 { + margin: 0; + font-weight: 600; + } +} + +.ckyc-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; +} + +.ckyc-info-card { + margin-bottom: 24px; +} + +.ckyc-info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + align-items: start; +} + +.ckyc-info-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.ckyc-info-label { + font-size: 11px; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.ckyc-info-value { + font-size: 14px; + color: #333; + min-height: 20px; +} + +.ckyc-section-title { + margin: 24px 0 12px; + font-weight: 600; +} + +.ckyc-upload-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 16px; + margin-bottom: 16px; +} + +.ckyc-upload-card { + .mat-mdc-card-content { + padding: 16px; + } +} + +.ckyc-upload-header { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 14px; + margin-bottom: 8px; +} + +.ckyc-upload-icon { + font-size: 20px; + height: 20px; + width: 20px; + color: #1976d2; +} + +.ckyc-upload-hint { + font-size: 12px; + color: #888; + margin: 0 0 12px; +} + +.ckyc-file-row { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 12px; +} + +.ckyc-file-name-field { + flex: 1; +} + +.ckyc-upload-error { + display: flex; + align-items: center; + gap: 6px; + margin-top: 10px; + padding: 8px 12px; + background-color: #fdecea; + border: 1px solid #f5c6cb; + border-radius: 6px; + font-size: 12px; + color: #b71c1c; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } +} + +.ckyc-upload-response { + margin-top: 12px; + padding: 10px 14px; + background-color: #e8f5e9; + border: 1px solid #a5d6a7; + border-radius: 8px; + font-size: 12px; + + p { + margin: 4px 0; + } +} + +@media (width <= 600px) { + .ckyc-info-grid { + grid-template-columns: 1fr; + } + + .ckyc-upload-grid { + grid-template-columns: 1fr; + } + + .ckyc-file-row { + flex-direction: column; + } + + .ckyc-file-name-field { + width: 100%; + } +} diff --git a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts new file mode 100644 index 000000000..5f66d01bc --- /dev/null +++ b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts @@ -0,0 +1,219 @@ +/** + * 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, OnInit, OnDestroy, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subject, takeUntil, catchError, of } from 'rxjs'; + +/** Custom Services */ +import { ClientsService } from '../../clients.service'; +import { AlertService } from 'app/core/alert/alert.service'; +import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-ckyc-tab', + templateUrl: './ckyc-tab.component.html', + styleUrls: ['./ckyc-tab.component.scss'], + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatIcon, + MatProgressSpinner, + FaIconComponent + ] +}) +export class CkycTabComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private clientsService = inject(ClientsService); + private alertService = inject(AlertService); + + private destroy$ = new Subject(); + + /** Top section - read-only client info */ + clientId: string; + clientName: string = ''; + allAccounts: any[] = []; + selectedAccount: any = null; + selectedAccountType: string = ''; + + /** State */ + loading = true; + ckycData: any = null; + + /** File references */ + addressProofFile: File | null = null; + photoProofFile: File | null = null; + clientImageFile: File | null = null; + clientSignatureFile: File | null = null; + + /** Per-document upload state */ + uploading: { [key: string]: boolean } = { + ADDRESS_PROOF: false, + PHOTO_PROOF: false, + CLIENT_IMAGE: false, + CLIENT_SIGNATURE: false + }; + + /** Per-document upload response */ + uploadResponses: { [key: string]: any } = {}; + + /** Per-document error messages */ + uploadErrors: { [key: string]: string | null } = { + ADDRESS_PROOF: null, + PHOTO_PROOF: null, + CLIENT_IMAGE: null, + CLIENT_SIGNATURE: null + }; + + ngOnInit(): void { + this.route.parent.params.subscribe((params: any) => { + this.clientId = params['clientId']; + this.loadClientInfo(); + this.loadAccounts(); + this.loadCkycData(); + }); + } + + private loadClientInfo(): void { + this.route.parent.data.subscribe((data: any) => { + if (data?.clientViewData) { + this.clientName = data.clientViewData.displayName || ''; + } + }); + } + + private loadAccounts(): void { + this.clientsService + .getClientAccountData(this.clientId) + .pipe( + takeUntil(this.destroy$), + catchError(() => of(null)) + ) + .subscribe((data: any) => { + if (data) { + const savings = (data.savingsAccounts || []).map((a: any) => ({ + accountNo: a.accountNo, + accountType: 'Savings', + productName: a.productName || '', + id: a.id + })); + const loans = (data.loanAccounts || []).map((a: any) => ({ + accountNo: a.accountNo, + accountType: 'Loan', + productName: a.productName || '', + id: a.id + })); + this.allAccounts = [ + ...savings, + ...loans + ]; + } + }); + } + + private loadCkycData(): void { + this.loading = true; + this.clientsService + .getClientCkyc(this.clientId) + .pipe( + takeUntil(this.destroy$), + catchError(() => { + this.loading = false; + return of(null); + }) + ) + .subscribe((data: any) => { + this.loading = false; + this.ckycData = data; + }); + } + + onAccountSelect(accountNo: string): void { + this.selectedAccount = this.allAccounts.find((a) => a.accountNo === accountNo) || null; + this.selectedAccountType = this.selectedAccount?.accountType || ''; + } + + onFileSelect(event: any, documentType: string): void { + const file = event.target?.files?.[0]; + if (!file) return; + + switch (documentType) { + case 'ADDRESS_PROOF': + this.addressProofFile = file; + break; + case 'PHOTO_PROOF': + this.photoProofFile = file; + break; + case 'CLIENT_IMAGE': + this.clientImageFile = file; + break; + case 'CLIENT_SIGNATURE': + this.clientSignatureFile = file; + break; + } + this.uploadErrors[documentType] = null; + } + + getFile(documentType: string): File | null { + switch (documentType) { + case 'ADDRESS_PROOF': + return this.addressProofFile; + case 'PHOTO_PROOF': + return this.photoProofFile; + case 'CLIENT_IMAGE': + return this.clientImageFile; + case 'CLIENT_SIGNATURE': + return this.clientSignatureFile; + default: + return null; + } + } + + uploadDocument(documentType: string): void { + const file = this.getFile(documentType); + if (!file) return; + + this.uploading[documentType] = true; + this.uploadErrors[documentType] = null; + + const formData = new FormData(); + formData.append('file', file); + formData.append('documentType', documentType); + + this.clientsService + .uploadCkycDocument(this.clientId, formData) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: any) => { + this.uploading[documentType] = false; + this.uploadResponses[documentType] = response; + this.alertService.alert({ + type: 'success', + message: response?.message || 'Document uploaded successfully.' + }); + }, + error: (err: any) => { + this.uploading[documentType] = false; + this.uploadErrors[documentType] = + err?.error?.developerMessage || + err?.error?.defaultUserMessage || + err?.message || + 'Failed to upload document.'; + } + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/src/app/clients/clients-view/clients-view.component.html b/src/app/clients/clients-view/clients-view.component.html index 3c8f9aa75..fb86db405 100644 --- a/src/app/clients/clients-view/clients-view.component.html +++ b/src/app/clients/clients-view/clients-view.component.html @@ -440,6 +440,21 @@ > {{ 'labels.inputs.Identities' | translate }} + + {{ 'labels.inputs.KYC' | translate }} + + + {{ 'labels.inputs.CKYC' | translate }} + + + {{ 'labels.inputs.Form 60' | translate }} + +

{{ 'labels.heading.Nominee Details (DA1 / DA5)' | translate }}

+
+ - {{ 'labels.inputs.First Name' | translate }} - + {{ 'labels.inputs.Nomination Type' | translate }} + + {{ 'labels.inputs.DA1 - Add Nominee' | translate }} + {{ 'labels.inputs.DA5 - Modify / Cancel Nominee' | translate }} + + + + + + {{ 'labels.inputs.Nominee Name' | translate }} + @if (addFamilyMemberForm.controls.firstName.hasError('required')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Nominee Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} } - - {{ 'labels.inputs.Middle Name' | translate }} - - - - - {{ 'labels.inputs.Last Name' | translate }} - - @if (addFamilyMemberForm.controls.lastName.hasError('required')) { - - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Qualification' | translate }} - - - - - {{ 'labels.inputs.Age' | translate }} - - - - - {{ 'labels.inputs.Is Dependent' | translate }}? - - + {{ 'labels.inputs.Relationship' | translate }} @@ -102,46 +50,8 @@ } - - {{ 'labels.inputs.Gender' | translate }} - - @for (gender of addFamilyMemberTemplate.genderIdOptions; track gender) { - - {{ gender.name | translateKey: 'catalogs' }} - - } - - @if (addFamilyMemberForm.controls.genderId.hasError('required')) { - - {{ 'labels.inputs.Gender' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Profession' | translate }} - - @for (profession of addFamilyMemberTemplate.professionIdOptions; track profession) { - - {{ profession.name | translateKey: 'catalogs' }} - - } - - - - - {{ 'labels.inputs.Marital Status' | translate }} - - @for (maritalStatus of addFamilyMemberTemplate.maritalStatusIdOptions; track maritalStatus) { - - {{ maritalStatus.name | translateKey: 'catalogs' }} - - } - - - - + + {{ 'labels.inputs.Date Of Birth' | translate }} + + + + {{ 'labels.inputs.Percentage Share' | translate }} (%) + + + + + + {{ 'labels.inputs.Nominee is Minor' | translate }}? + +
+ + + @if (addFamilyMemberForm.get('isDependent').value) { +

{{ 'labels.heading.Guardian Details (If Minor)' | translate }}

+
+ + {{ 'labels.inputs.Guardian Name' | translate }} + + + + + {{ 'labels.inputs.Relationship with Minor' | translate }} + + +
+ } + + +
+ + {{ 'labels.inputs.Remarks' | translate }} + +
diff --git a/src/app/clients/clients-view/family-members-tab/add-family-member/add-family-member.component.ts b/src/app/clients/clients-view/family-members-tab/add-family-member/add-family-member.component.ts index 3dd3c0f8e..a1a175d25 100644 --- a/src/app/clients/clients-view/family-members-tab/add-family-member/add-family-member.component.ts +++ b/src/app/clients/clients-view/family-members-tab/add-family-member/add-family-member.component.ts @@ -40,8 +40,6 @@ export class AddFamilyMemberComponent implements OnInit { /** Maximum Due Date allowed. */ maxDate = new Date(); - /** Minimum age allowed is 0. */ - minAge = 0; /** Add family member form. */ addFamilyMemberForm: UntypedFormGroup; /** Add family member template. */ @@ -49,14 +47,6 @@ export class AddFamilyMemberComponent implements OnInit { /** Client ID */ clientId: any; - /** - * @param {FormBuilder} formBuilder FormBuilder - * @param {Dates} dateUtils Date Utils - * @param {Router} router Router - * @param {Route} route Route - * @param {ClientsService} clientsService Clients Service - * @param {SettingsService} settingsService Setting service - */ constructor() { this.route.data.subscribe((data: { clientTemplate: any }) => { this.addFamilyMemberTemplate = data.clientTemplate.familyMemberOptions; @@ -67,91 +57,39 @@ export class AddFamilyMemberComponent implements OnInit { ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createAddFamilyMemberForm(); - this.addFamilyMemberForm.get('dateOfBirth').valueChanges.subscribe((dateOfBirth: any) => { - if (dateOfBirth) { - const age = this.calculateAge(dateOfBirth); - this.addFamilyMemberForm.get('age').setValue(age); - } else { - this.addFamilyMemberForm.get('age').setValue(''); - } - }); } - /** - * 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; - } - - /** - * Creates the add family member form - */ createAddFamilyMemberForm() { this.addFamilyMemberForm = 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: [''] }); } - /** - * Submits the form and adds the family member - */ submit() { - // Get form values including disabled controls like age - const formValue = { - ...this.addFamilyMemberForm.getRawValue() - }; + const formValue = { ...this.addFamilyMemberForm.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) { 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); + formValue.dateOfBirth = this.dateUtils.formatDate(formValue.dateOfBirth, dateFormat); } } else { - // If no date of birth, remove age and dateOfBirth from submission - delete formValue.age; delete formValue.dateOfBirth; } diff --git a/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.html b/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.html index ce300efa1..cdd131a5f 100644 --- a/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.html +++ b/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.html @@ -8,48 +8,31 @@
+

{{ 'labels.heading.Nominee Details (DA1 / DA5)' | translate }}

+
+ - {{ 'labels.inputs.First Name' | translate }} + {{ 'labels.inputs.Nomination Type' | translate }} + + {{ 'labels.inputs.DA1 - Add Nominee' | translate }} + {{ 'labels.inputs.DA5 - Modify / Cancel Nominee' | translate }} + + + + + + {{ 'labels.inputs.Nominee Name' | translate }} @if (editFamilyMemberForm.controls.firstName.hasError('required')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Nominee Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} } - - {{ 'labels.inputs.Middle Name' | translate }} - - - - - {{ 'labels.inputs.Last Name' | translate }} - - @if (editFamilyMemberForm.controls.lastName.hasError('required')) { - - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Qualification' | translate }} - - - - - {{ 'labels.inputs.Age' | translate }} - - - - - {{ 'labels.inputs.Is Dependent' | translate }} - - + {{ 'labels.inputs.Relationship' | translate }} @@ -67,46 +50,8 @@ } - - {{ 'labels.inputs.Gender' | translate }} - - @for (gender of addFamilyMemberTemplate.genderIdOptions; track gender) { - - {{ gender.name | translateKey: 'catalogs' }} - - } - - @if (editFamilyMemberForm.controls.genderId.hasError('required')) { - - {{ 'labels.inputs.Gender' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Profession' | translate }} - - @for (profession of addFamilyMemberTemplate.professionIdOptions; track profession) { - - {{ profession.name | translateKey: 'catalogs' }} - - } - - - - - {{ 'labels.inputs.Marital Status' | translate }} - - @for (maritalStatus of addFamilyMemberTemplate.maritalStatusIdOptions; track maritalStatus) { - - {{ maritalStatus.name | translateKey: 'catalogs' }} - - } - - - - + + {{ 'labels.inputs.Date Of Birth' | translate }} + + + + {{ 'labels.inputs.Percentage Share' | translate }} (%) + + + + + + {{ 'labels.inputs.Nominee is Minor' | translate }}? + +
+ + + @if (editFamilyMemberForm.get('isDependent').value) { +

{{ 'labels.heading.Guardian Details (If Minor)' | translate }}

+
+ + {{ 'labels.inputs.Guardian Name' | translate }} + + + + + {{ 'labels.inputs.Relationship with Minor' | translate }} + + +
+ } + + +
+ + {{ 'labels.inputs.Remarks' | translate }} + +
diff --git a/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.ts b/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.ts index 9cb5ecfae..d55700c2e 100644 --- a/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.ts +++ b/src/app/clients/clients-view/family-members-tab/edit-family-member/edit-family-member.component.ts @@ -40,21 +40,13 @@ export class EditFamilyMemberComponent implements OnInit { /** Maximum Due Date allowed. */ maxDate = new Date(); - /** Add family member form. */ + /** Edit family member form. */ editFamilyMemberForm: UntypedFormGroup; - /** Add family member template. */ + /** Family member template options. */ addFamilyMemberTemplate: any; /** Family Members Details */ familyMemberDetails: any; - /** - * @param {FormBuilder} formBuilder Form Builder - * @param {Dates} dateUtils Date Utils - * @param {Router} router Router - * @param {ActivatedRoute} route Route - * @param {ClientsService} clientsService Clients Service - * @param {SettingsService} settingsService Setting service - */ constructor() { this.route.data.subscribe((data: { clientTemplate: any; editFamilyMember: any }) => { this.addFamilyMemberTemplate = data.clientTemplate.familyMemberOptions; @@ -65,94 +57,41 @@ export class EditFamilyMemberComponent implements OnInit { ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createEditFamilyMemberForm(this.familyMemberDetails); - this.editFamilyMemberForm.get('dateOfBirth').valueChanges.subscribe((dateOfBirth: any) => { - if (dateOfBirth) { - const age = this.calculateAge(dateOfBirth); - this.editFamilyMemberForm.get('age').setValue(age); - } else { - this.editFamilyMemberForm.get('age').setValue(''); - } - }); } - /** - * 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; - } - - /** - * Creates Edit Family Member Form - * @param {any} familyMember Family Member - */ createEditFamilyMemberForm(familyMember: any) { this.editFamilyMemberForm = this.formBuilder.group({ + nominationType: [familyMember.nominationType || 'DA1'], firstName: [ familyMember.firstName, Validators.required ], - middleName: [familyMember.middleName], - lastName: [ - familyMember.lastName, - Validators.required - ], - qualification: [familyMember.qualification], - age: [ - { value: familyMember.age, disabled: true }], - isDependent: [familyMember.isDependent], relationshipId: [ familyMember.relationshipId, Validators.required ], - genderId: [ - familyMember.genderId, - Validators.required - ], - professionId: [familyMember.professionId], - maritalStatusId: [familyMember.maritalStatusId], dateOfBirth: [ familyMember.dateOfBirth ? this.dateUtils.formatDate(familyMember.dateOfBirth, 'yyyy-MM-dd') : null - ] + ], + percentageShare: [familyMember.percentageShare ?? 100], + isDependent: [familyMember.isDependent || false], + guardianName: [familyMember.guardianName || ''], + guardianRelationship: [familyMember.guardianRelationship || ''], + remarks: [familyMember.remarks || ''] }); } - /** - * Submits the form and updates the client family member. - */ submit() { - // Get form values including disabled controls like age - const formValue = { - ...this.editFamilyMemberForm.getRawValue() - }; + const formValue = { ...this.editFamilyMemberForm.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) { 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); + formValue.dateOfBirth = this.dateUtils.formatDate(formValue.dateOfBirth, dateFormat); } } else { - // If no date of birth, remove age and dateOfBirth from submission - delete formValue.age; delete formValue.dateOfBirth; } diff --git a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html index 18b2dc35a..e588f5de7 100644 --- a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html +++ b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html @@ -51,39 +51,35 @@

+ @if (member.nominationType) { + {{ 'labels.inputs.Nomination Type' | translate }} : {{ member.nominationType }}
+ } @if (member.firstName) { - {{ 'labels.inputs.First Name' | translate }} : {{ member.firstName }}
+ {{ 'labels.inputs.Nominee Name' | translate }} : {{ member.firstName }}
} - @if (member.middleName) { - {{ 'labels.inputs.Middle Name' | translate }} : {{ member.middleName }}
- } - @if (member.lastName) { - {{ 'labels.inputs.Last Name' | translate }} : {{ member.lastName }}
- } - @if (member.qualification) { - {{ 'labels.inputs.Qualification' | translate }} : {{ member.qualification }}
- } - @if (member.mobileNumber) { - {{ 'labels.inputs.Mobile Number' | translate }} : {{ member.mobileNumber }}
- } - @if (member.age) { - {{ 'labels.inputs.Age' | translate }} : {{ member.age }}
- } - @if (member.isDependent) { - {{ 'labels.inputs.Is Dependent' | translate }} : {{ member.isDependent | yesNo }}
- } - @if (member.maritalStatus) { - {{ 'labels.inputs.Marital Status' | translate }} : {{ member.maritalStatus }}
- } - @if (member.gender) { - {{ 'labels.inputs.Gender' | translate }} : {{ member.gender }}
- } - @if (member.profession) { - {{ 'labels.inputs.Profession' | translate }} : {{ member.profession }}
+ @if (member.relationship) { + {{ 'labels.inputs.Relationship' | translate }} : {{ member.relationship }}
} @if (member.dateOfBirth) { {{ 'labels.inputs.Date Of Birth' | translate }} : {{ member.dateOfBirth | dateFormat }}
} + @if (member.percentageShare != null) { + {{ 'labels.inputs.Percentage Share' | translate }} : {{ member.percentageShare }}%
+ } + @if (member.isDependent) { + {{ 'labels.inputs.Nominee is Minor' | translate }} : {{ member.isDependent | yesNo }}
+ } + @if (member.guardianName) { + {{ 'labels.inputs.Guardian Name' | translate }} : {{ member.guardianName }}
+ } + @if (member.guardianRelationship) { + {{ 'labels.inputs.Relationship with Minor' | translate }} : {{ member.guardianRelationship }}
+ } + @if (member.remarks) { + {{ 'labels.inputs.Remarks' | translate }} : {{ member.remarks }}
+ }

} diff --git a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts index 09b97895c..ba18e24b5 100644 --- a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts +++ b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts @@ -74,7 +74,7 @@ export class FamilyMembersTabComponent { */ deleteFamilyMember(clientId: string, id: string, name: string, index: number) { const deleteFamilyMemberDialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `Family member id:${id} name : ${name} ${index}` } + data: { deleteContext: `Nominee id:${id} name : ${name}` } }); deleteFamilyMemberDialogRef.afterClosed().subscribe((response: any) => { if (response.delete) { @@ -86,13 +86,6 @@ export class FamilyMembersTabComponent { } displayName(member: any): string { - let fullName: string = member.firstName; - if (member.middleName) { - fullName = fullName + ' ' + member.middleName; - } - if (member.lastName) { - fullName = fullName + ' ' + member.lastName; - } - return fullName; + return member.firstName || ''; } } diff --git a/src/app/clients/clients-view/form60-tab/form60-tab.component.html b/src/app/clients/clients-view/form60-tab/form60-tab.component.html new file mode 100644 index 000000000..368088722 --- /dev/null +++ b/src/app/clients/clients-view/form60-tab/form60-tab.component.html @@ -0,0 +1,185 @@ + + +
+ +
+ warning + {{ 'labels.text.PAN not provided. Please fill Form 60 as per Income Tax Rule 114B.' | translate }} +
+ + @if (loading) { +
+ +
+ } @else { + + @if (form60Data && !isEditMode) { + + + {{ 'labels.inputs.Form 60 Declaration' | translate }} + + +
+ @if (form60Data.fullName) { +
+ {{ 'labels.inputs.Full Name' | translate }} + {{ form60Data.fullName }} +
+ } + @if (form60Data.dateOfBirth) { +
+ {{ 'labels.inputs.Date Of Birth' | translate }} + {{ form60Data.dateOfBirth | dateFormat }} +
+ } + @if (form60Data.fatherName) { +
+ {{ "labels.inputs.Father's Name" | translate }} + {{ form60Data.fatherName }} +
+ } + @if (form60Data.address) { +
+ {{ 'labels.inputs.Address' | translate }} + {{ form60Data.address }} +
+ } + @if (form60Data.transactionType) { +
+ {{ 'labels.inputs.Transaction Type' | translate }} + {{ form60Data.transactionType }} +
+ } + @if (form60Data.transactionAmount) { +
+ {{ 'labels.inputs.Transaction Amount' | translate }} + {{ form60Data.transactionAmount }} +
+ } + @if (form60Data.estimatedTotalIncome) { +
+ {{ 'labels.inputs.Estimated Total Income' | translate }} + {{ form60Data.estimatedTotalIncome }} +
+ } + @if (form60Data.reasonForNoPan) { +
+ {{ 'labels.inputs.Reason for Not Having PAN' | translate }} + {{ form60Data.reasonForNoPan }} +
+ } +
+
+ + + +
+ } + + + @if (isEditMode) { + + + {{ 'labels.inputs.Form 60 Declaration' | translate }} + + + +
+ + + {{ 'labels.inputs.Full Name' | translate }} + + @if (form60Form.controls.fullName.hasError('required')) { + + {{ 'labels.inputs.Full Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + {{ 'labels.inputs.Date Of Birth' | translate }} + + + + + + + + {{ "labels.inputs.Father's Name" | translate }} + + + + + + {{ 'labels.inputs.Address' | translate }} + + + + + + {{ 'labels.inputs.Transaction Type' | translate }} + + @for (txnType of transactionTypes; track txnType.value) { + {{ txnType.key | translate }} + } + + + + + + {{ 'labels.inputs.Transaction Amount' | translate }} + + + + + + {{ 'labels.inputs.Estimated Total Income' | translate }} + + + + + + {{ 'labels.inputs.Reason for Not Having PAN' | translate }} + + @for (reason of reasonOptions; track reason.value) { + {{ reason.key | translate }} + } + + +
+ + +
+ + {{ 'labels.text.I declare that I do not have PAN and information provided is correct.' | translate }} + +
+ + +
+ @if (form60Data) { + + } + +
+ +
+
+ } + } +
diff --git a/src/app/clients/clients-view/form60-tab/form60-tab.component.scss b/src/app/clients/clients-view/form60-tab/form60-tab.component.scss new file mode 100644 index 000000000..9e685ad4e --- /dev/null +++ b/src/app/clients/clients-view/form60-tab/form60-tab.component.scss @@ -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/. + */ + +.form60-container { + padding: 16px; +} + +.form60-notice { + display: flex; + align-items: center; + gap: 10px; + background: #fff3cd; + padding: 12px 16px; + border-left: 5px solid #fc0; + border-radius: 4px; + margin-bottom: 16px; + font-size: 14px; + color: #856404; + + mat-icon { + color: #856404; + } +} + +.form60-loading { + display: flex; + justify-content: center; + padding: 32px; +} + +.form60-summary { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + padding: 16px 0; +} + +.form60-field { + .label { + font-size: 12px; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; + display: block; + } + + .value { + font-size: 14px; + color: #333; + margin-top: 4px; + display: block; + } +} + +.form60-declaration { + margin: 16px 0 24px; + padding: 12px 16px; + background: #f5f5f5; + border-radius: 6px; +} + +@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; + } +} + +@media (width <= 768px) { + .form60-summary { + grid-template-columns: 1fr; + } +} diff --git a/src/app/clients/clients-view/form60-tab/form60-tab.component.ts b/src/app/clients/clients-view/form60-tab/form60-tab.component.ts new file mode 100644 index 000000000..9350c62a5 --- /dev/null +++ b/src/app/clients/clients-view/form60-tab/form60-tab.component.ts @@ -0,0 +1,194 @@ +/** + * 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, OnInit, OnDestroy, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Subject, takeUntil, catchError, of } from 'rxjs'; + +/** Custom Services */ +import { ClientsService } from '../../clients.service'; +import { AlertService } from 'app/core/alert/alert.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DateFormatPipe } from '../../../pipes/date-format.pipe'; + +@Component({ + selector: 'mifosx-form60-tab', + templateUrl: './form60-tab.component.html', + styleUrls: ['./form60-tab.component.scss'], + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatIcon, + MatProgressSpinner, + DateFormatPipe + ] +}) +export class Form60TabComponent implements OnInit, OnDestroy { + private formBuilder = inject(UntypedFormBuilder); + private route = inject(ActivatedRoute); + private clientsService = inject(ClientsService); + private alertService = inject(AlertService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + private destroy$ = new Subject(); + + /** Client info */ + clientId: string; + + /** State */ + loading = true; + form60Data: any = null; + isEditMode = false; + + /** Maximum date allowed */ + maxDate = new Date(); + + /** Form */ + form60Form: UntypedFormGroup; + + /** Dropdown options */ + transactionTypes = [ + { value: 'Account Opening', key: 'labels.inputs.Account Opening' }, + { value: 'Cash Deposit', key: 'labels.inputs.Cash Deposit' }, + { value: 'Fixed Deposit', key: 'labels.inputs.Fixed Deposit' }, + { value: 'Loan Processing', key: 'labels.inputs.Loan Processing' } + ]; + + reasonOptions = [ + { value: 'Applied for PAN', key: 'labels.inputs.Applied for PAN' }, + { value: 'Income below taxable limit', key: 'labels.inputs.Income below taxable limit' }, + { value: 'Other', key: 'labels.inputs.Other' } + ]; + + ngOnInit(): void { + this.maxDate = this.settingsService.businessDate; + this.createForm(); + this.route.parent.params.subscribe((params: any) => { + this.clientId = params['clientId']; + this.loadForm60Data(); + }); + } + + private createForm(): void { + this.form60Form = this.formBuilder.group({ + fullName: [ + '', + Validators.required + ], + dateOfBirth: [''], + fatherName: [''], + address: [''], + transactionType: ['Account Opening'], + transactionAmount: [''], + estimatedTotalIncome: [''], + reasonForNoPan: ['Applied for PAN'], + declaration: [ + false, + Validators.requiredTrue + ] + }); + } + + private loadForm60Data(): void { + this.loading = true; + this.clientsService + .getClientForm60(this.clientId) + .pipe( + takeUntil(this.destroy$), + catchError(() => { + this.loading = false; + return of(null); + }) + ) + .subscribe((data: any) => { + this.loading = false; + this.form60Data = data; + if (data) { + this.populateForm(data); + } else { + this.isEditMode = true; + } + }); + } + + private populateForm(data: any): void { + this.form60Form.patchValue({ + fullName: data.fullName || '', + dateOfBirth: data.dateOfBirth ? this.dateUtils.formatDate(data.dateOfBirth, 'yyyy-MM-dd') : '', + fatherName: data.fatherName || '', + address: data.address || '', + transactionType: data.transactionType || 'Account Opening', + transactionAmount: data.transactionAmount || '', + estimatedTotalIncome: data.estimatedTotalIncome || '', + reasonForNoPan: data.reasonForNoPan || 'Applied for PAN', + declaration: data.declaration || false + }); + } + + toggleEditMode(): void { + this.isEditMode = !this.isEditMode; + if (!this.isEditMode && this.form60Data) { + this.populateForm(this.form60Data); + } + } + + submit(): void { + const formValue = { ...this.form60Form.value }; + + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.dateOfBirth) { + if (formValue.dateOfBirth instanceof Date) { + formValue.dateOfBirth = this.dateUtils.formatDate(formValue.dateOfBirth, dateFormat); + } + } else { + delete formValue.dateOfBirth; + } + + const data = { + ...formValue, + dateFormat, + locale + }; + + this.clientsService + .submitClientForm60(this.clientId, data) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: any) => { + this.form60Data = response || data; + this.isEditMode = false; + this.alertService.alert({ + type: 'success', + message: 'Form 60 submitted successfully.' + }); + }, + error: (err: any) => { + this.alertService.alert({ + type: 'error', + message: err?.error?.developerMessage || err?.error?.defaultUserMessage || 'Failed to submit Form 60.' + }); + } + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html new file mode 100644 index 000000000..7a40792e4 --- /dev/null +++ b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html @@ -0,0 +1,621 @@ + + +
+ + @if (loading) { +
+ +

{{ 'labels.inputs.KYC Verification' | translate }}...

+
+ } + + @if (!loading) { + +
+
+

{{ 'labels.inputs.KYC Verification' | translate }}

+ KYC + @if (kycData?.kycStatusCode) { + + {{ kycData.kycStatusCode }} + + } +
+ +
+ + + @if (!isEditMode && kycData) { +
+ + + +
+
+ credit_card + {{ 'labels.inputs.PAN Number' | translate }} + KYC +
+ @if (kycData.panVerified) { + + check_circle + {{ 'labels.inputs.Verified' | translate }} + + } @else { + + radio_button_unchecked + {{ 'labels.inputs.Not Verified' | translate }} + + } +
+

{{ kycData.panNumber || '—' }}

+ @if (kycData.panVerifiedOn) { +

+ {{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.panVerifiedOn) }} +

+ } + @if (verificationError.pan) { +
+ error_outline + {{ verificationError.pan }} +
+ } + @if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) { +

{{ verificationDetails.pan.name }}

+ } + +
+
+ + + + +
+
+ fingerprint + {{ 'labels.inputs.Aadhaar Number' | translate }} + KYC +
+ @if (kycData.aadhaarVerified) { + + check_circle + {{ 'labels.inputs.Verified' | translate }} + + } @else { + + radio_button_unchecked + {{ 'labels.inputs.Not Verified' | translate }} + + } +
+

{{ kycData.aadhaarNumberMasked || '—' }}

+ @if (kycData.aadhaarVerifiedOn) { +

+ {{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.aadhaarVerifiedOn) }} +

+ } + @if (verificationError.aadhaar) { +
+ error_outline + {{ verificationError.aadhaar }} +
+ } + @if (verificationStatus.aadhaar === 'VERIFIED' && verificationDetails.aadhaar?.name) { +

{{ verificationDetails.aadhaar.name }}

+ } + + + @if (!showAadhaarReVerify && !aadhaarOtpSent) { + + } + + + @if (showAadhaarReVerify && !aadhaarOtpSent) { +
+
+ + {{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }} + + + +
+
+ } + + + @if (aadhaarOtpSent) { +
+

+ sms + OTP sent to Aadhaar-linked mobile +

+
+ + Enter OTP + + + + +
+
+ } +
+
+ + + + +
+
+ business + {{ 'labels.inputs.GSTIN Number' | translate }} + KYC +
+ @if (kycData.gstinVerified) { + + check_circle + {{ 'labels.inputs.Verified' | translate }} + + } @else { + + radio_button_unchecked + {{ 'labels.inputs.Not Verified' | translate }} + + } +
+

{{ kycData.gstinNumber || '—' }}

+ @if (kycData.gstinVerifiedOn) { +

+ {{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.gstinVerifiedOn) }} +

+ } + @if (verificationError.gstin) { +
+ error_outline + {{ verificationError.gstin }} +
+ } + @if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) { +

{{ verificationDetails.gstin.businessName }}

+ } + +
+
+
+ + + + +
+
+ {{ 'labels.inputs.KYC Type' | translate }} + {{ + kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' + }} +
+
+ {{ 'labels.inputs.Risk Category' | translate }} + {{ + kycData.riskCategory === 1 + ? 'Low' + : kycData.riskCategory === 2 + ? 'Medium' + : kycData.riskCategory === 3 + ? 'High' + : '—' + }} +
+
+ {{ 'labels.inputs.Initiated On' | translate }} + {{ kycData.initiatedOn || '—' }} +
+
+ {{ 'labels.inputs.Last Verified On' | translate }} + {{ formatDate(kycData.verifiedOn) }} +
+
+ {{ 'labels.inputs.Expiry Date' | translate }} + {{ kycData.expiryDate || '—' }} +
+ @if (kycData.remarks) { +
+ {{ 'labels.inputs.Remarks' | translate }} + {{ kycData.remarks }} +
+ } +
+
+
+ } + + + @if (!isEditMode && !kycData) { +
+ verified_user +

{{ 'labels.inputs.KYC Verification' | translate }} not initiated yet.

+ +
+ } + + + @if (isEditMode) { + + +
+
+ +
+
+ {{ 'labels.inputs.PAN Number' | translate }} + KYC +
+
+ + {{ 'labels.inputs.PAN Number' | translate }} + + @if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) { + {{ 'labels.inputs.Enter 10-character PAN' | translate }} + } + @if (verificationStatus.pan === 'VERIFIED') { + check_circle + } + @if (verificationStatus.pan === 'FAILED') { + cancel + } + + +
+ @if (verificationError.pan) { +
+ error_outline + {{ verificationError.pan }} +
+ } + @if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) { + {{ verificationDetails.pan.name }} + } +
+ + +
+
+ {{ 'labels.inputs.Aadhaar Number' | translate }} + KYC +
+
+ + {{ 'labels.inputs.Aadhaar Number' | translate }} + + @if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) { + {{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }} + } + @if (verificationStatus.aadhaar === 'VERIFIED') { + check_circle + } + @if (verificationStatus.aadhaar === 'FAILED') { + cancel + } + + @if (!aadhaarOtpSent) { + + } +
+ + + @if (aadhaarOtpSent) { +
+

+ sms + OTP sent to Aadhaar-linked mobile +

+
+ + Enter OTP + + + + +
+
+ } + + @if (verificationError.aadhaar) { +
+ error_outline + {{ verificationError.aadhaar }} +
+ } +
+ + +
+
+ {{ 'labels.inputs.GSTIN Number' | translate }} + KYC +
+
+ + {{ 'labels.inputs.GSTIN Number' | translate }} + + @if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) { + {{ 'labels.inputs.Enter 15-character GSTIN' | translate }} + } + @if (verificationStatus.gstin === 'VERIFIED') { + check_circle + } + @if (verificationStatus.gstin === 'FAILED') { + cancel + } + + +
+ @if (verificationError.gstin) { +
+ error_outline + {{ verificationError.gstin }} +
+ } + @if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) { + {{ verificationDetails.gstin.businessName }} + } +
+ + + @if (!kycId) { + + {{ 'labels.inputs.KYC Type' | translate }} + + Individual + Non-Individual + + + } + + + + {{ 'labels.inputs.Risk Category' | translate }} + + Low + Medium + High + + + + + + {{ 'labels.inputs.Remarks' | translate }} + + +
+ + @if (errorMessage) { +
+ error_outline + {{ errorMessage }} +
+ } + + @if (!kycId) { +
+ info + Save KYC details first to enable verification buttons. +
+ } + +
+ + +
+
+
+
+ } + } +
diff --git a/src/app/clients/clients-view/kyc-tab/kyc-tab.component.scss b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.scss new file mode 100644 index 000000000..152cbfc42 --- /dev/null +++ b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.scss @@ -0,0 +1,371 @@ +/** + * 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-tab-container { + padding: 16px; +} + +.kyc-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + gap: 16px; + color: #666; +} + +.kyc-tab-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.kyc-tab-title { + display: flex; + align-items: center; + gap: 10px; + + 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-overall-status { + font-size: 12px; + font-weight: 600; + padding: 3px 10px; + border-radius: 12px; + + &.status-verified, + &.status-complete { + background-color: #e8f5e9; + color: #2e7d32; + border: 1px solid #a5d6a7; + } + + &.status-pending, + &.status-initiated { + background-color: #fff8e1; + color: #f57f17; + border: 1px solid #ffe082; + } + + &.status-rejected, + &.status-failed { + background-color: #ffebee; + color: #c62828; + border: 1px solid #ef9a9a; + } +} + +.kyc-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 16px; +} + +.kyc-card { + .mat-mdc-card-content { + padding: 16px; + } +} + +.kyc-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.kyc-card-label { + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + font-size: 14px; +} + +.kyc-card-icon { + font-size: 18px; + height: 18px; + width: 18px; + color: #1976d2; +} + +.kyc-value { + font-size: 16px; + font-weight: 500; + letter-spacing: 1px; + margin: 8px 0 4px; + font-family: monospace; +} + +.kyc-meta { + font-size: 12px; + color: #888; + margin: 0 0 8px; +} + +.re-verify-btn { + font-size: 12px; + padding: 0 12px; + height: 28px; + line-height: 28px; +} + +.status-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 500; + padding: 3px 8px; + border-radius: 14px; + + &.verified { + background-color: #e8f5e9; + color: #2e7d32; + border: 1px solid #a5d6a7; + } + + &.not-verified { + background-color: #f5f5f5; + color: #757575; + border: 1px solid #e0e0e0; + } +} + +.status-chip-icon { + font-size: 14px; + height: 14px; + width: 14px; +} + +.kyc-info-card { + margin-bottom: 16px; +} + +.kyc-info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +.kyc-info-item { + display: flex; + flex-direction: column; + gap: 2px; + + &.full-span { + grid-column: 1 / -1; + } +} + +.kyc-info-label { + font-size: 11px; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.kyc-info-value { + font-size: 14px; + color: #333; +} + +.kyc-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 16px; + gap: 16px; + color: #888; + text-align: center; +} + +.kyc-empty-icon { + font-size: 64px; + height: 64px; + width: 64px; + color: #bdbdbd; +} + +.kyc-edit-card { + max-width: 900px; +} + +.kyc-edit-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.kyc-edit-field { + display: flex; + flex-direction: column; +} + +.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: 10px; +} + +.kyc-input { + flex: 1; +} + +.verify-btn { + margin-top: 4px; + min-width: 90px; +} + +.full-span { + grid-column: 1 / -1; +} + +.kyc-form-actions { + display: flex; + gap: 12px; + margin-top: 16px; + align-items: center; +} + +/* OTP Section */ +.otp-section { + margin-top: 8px; + padding: 10px 14px; + background-color: #f3f8ff; + border: 1px solid #bbdefb; + border-radius: 8px; +} + +.otp-info { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #1565c0; + margin: 0 0 8px; + font-weight: 500; +} + +.otp-info-icon { + font-size: 16px; + height: 16px; + width: 16px; + color: #1565c0; +} + +.otp-inline { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.otp-field { + max-width: 160px; +} + +.otp-verify-btn { + margin-top: 4px; + font-size: 12px; + height: 36px; +} + +.otp-resend-btn { + margin-top: 4px; + font-size: 11px; + height: 36px; +} + +/* Error message */ +.kyc-error-msg { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: #c62828; + margin-top: 6px; + padding: 5px 8px; + background-color: #ffebee; + border: 1px solid #ef9a9a; + border-radius: 6px; +} + +.kyc-error-icon { + font-size: 14px; + height: 14px; + width: 14px; + color: #c62828; +} + +.verification-detail-text { + display: block; + font-size: 12px; + color: #2e7d32; + font-weight: 500; + margin-top: 4px; +} + +/* Status icons in edit mode */ +.status-icon { + font-size: 20px; + + &.verified { + color: #2e7d32; + } + + &.failed { + color: #c62828; + } +} diff --git a/src/app/clients/clients-view/kyc-tab/kyc-tab.component.ts b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.ts new file mode 100644 index 000000000..cd625dbe9 --- /dev/null +++ b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.ts @@ -0,0 +1,426 @@ +/** + * 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, OnInit, OnDestroy, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms'; +import { catchError, of, Subject, takeUntil } from 'rxjs'; + +/** Custom Services */ +import { ClientsService } from '../../clients.service'; +import { Dates } from 'app/core/utils/dates'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * KYC Tab Component. + * Displays and manages KYC (Know Your Customer) details for an existing client. + * API: GET/POST /clients/{clientId}/kyc + */ +@Component({ + selector: 'mifosx-kyc-tab', + templateUrl: './kyc-tab.component.html', + styleUrls: ['./kyc-tab.component.scss'], + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + MatIcon, + MatProgressSpinner, + MatTooltip, + FaIconComponent + ] +}) +export class KycTabComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private clientsService = inject(ClientsService); + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + + private destroy$ = new Subject(); + + clientId: string; + kycId: string | null = null; + kycData: any = null; + clientDateOfBirth: string | null = null; + loading = true; + saving = false; + isEditMode = false; + errorMessage: string | null = null; + + verifying: { pan: boolean; aadhaar: boolean; gstin: boolean } = { + pan: false, + aadhaar: false, + gstin: false + }; + + /** Verification status tracking for re-verify */ + verificationStatus: { pan: string | null; aadhaar: string | null; gstin: string | null } = { + pan: null, + aadhaar: null, + gstin: null + }; + + verificationError: { pan: string | null; aadhaar: string | null; gstin: string | null } = { + pan: null, + aadhaar: null, + gstin: null + }; + + verificationDetails: { pan: any; aadhaar: any; gstin: any } = { + pan: null, + aadhaar: null, + gstin: null + }; + + /** Aadhaar OTP flow state */ + aadhaarOtpSent = false; + aadhaarOtpReferenceId: string | null = null; + aadhaarOtp = ''; + aadhaarOtpVerifying = false; + showAadhaarReVerify = false; + aadhaarFullNumber = ''; + + kycForm: UntypedFormGroup = this.formBuilder.group({ + kycType: [ + 1, + [Validators.required] + ], + riskCategory: [ + 1, + [Validators.required] + ], + 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: [''] + }); + + ngOnInit(): void { + this.route.parent.params.subscribe((params: any) => { + this.clientId = params['clientId']; + this.loadKycData(); + this.loadClientDateOfBirth(); + }); + } + + loadKycData(): void { + this.loading = true; + this.errorMessage = null; + this.clientsService + .getClientKyc(this.clientId) + .pipe( + catchError(() => { + this.loading = false; + return of(null); + }) + ) + .subscribe((data: any) => { + this.loading = false; + this.kycData = data; + if (data) { + this.kycId = data.id?.toString() || null; + this.kycForm.patchValue({ + kycType: data.kycType || 1, + riskCategory: data.riskCategory || 1, + panNumber: data.panNumber || '', + aadhaarNumber: data.aadhaarNumberMasked || '', + gstinNumber: data.gstinNumber || '', + remarks: data.remarks || '' + }); + } + }); + } + + /** Load client's date of birth from parent route resolved data */ + loadClientDateOfBirth(): void { + this.route.parent.data.subscribe((data: any) => { + if (data?.clientViewData?.dateOfBirth) { + const dob = data.clientViewData.dateOfBirth; + if (Array.isArray(dob) && dob.length >= 3) { + this.clientDateOfBirth = `${dob[0]}-${String(dob[1]).padStart(2, '0')}-${String(dob[2]).padStart(2, '0')}`; + } else if (typeof dob === 'string') { + this.clientDateOfBirth = dob; + } + } + }); + } + + toggleEditMode(): void { + this.isEditMode = !this.isEditMode; + this.resetOtpState(); + } + + saveKyc(): void { + if (this.kycForm.invalid) { + return; + } + this.saving = true; + this.errorMessage = null; + + const aadhaarRaw = this.kycForm.value.aadhaarNumber || ''; + const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : aadhaarRaw || null; + + const payload: any = { + locale: 'en', + panNumber: this.kycForm.value.panNumber || null, + aadhaarNumberMasked: aadhaarMasked, + gstinNumber: this.kycForm.value.gstinNumber || null, + remarks: this.kycForm.value.remarks || null + }; + + if (!this.kycId) { + payload.kycType = this.kycForm.value.kycType || 1; + payload.riskCategory = this.kycForm.value.riskCategory || 1; + } else { + payload.riskCategory = this.kycForm.value.riskCategory || 1; + } + + const save$ = this.kycId + ? this.clientsService.updateClientKyc(this.clientId, this.kycId, payload) + : this.clientsService.saveClientKyc(this.clientId, payload); + + save$.subscribe({ + next: (response: any) => { + this.saving = false; + this.isEditMode = false; + if (!this.kycId && response?.resourceId) { + this.kycId = response.resourceId.toString(); + } + this.loadKycData(); + }, + error: (err: any) => { + this.saving = false; + this.errorMessage = + err?.error?.developerMessage || + err?.error?.defaultUserMessage || + err?.message || + 'Failed to save KYC details.'; + } + }); + } + + verifyPAN(): void { + if (!this.kycId) { + this.verificationError.pan = 'KYC record not found. Save KYC details first.'; + return; + } + this.verifying.pan = true; + this.verificationStatus.pan = 'PENDING'; + this.verificationError.pan = null; + this.verificationDetails.pan = null; + + this.clientsService + .verifyPAN(this.clientId, this.kycId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: any) => { + this.verifying.pan = false; + if (response?.changes?.panVerified) { + this.verificationStatus.pan = 'VERIFIED'; + this.verificationDetails.pan = response.changes; + this.loadKycData(); + } else { + this.verificationStatus.pan = 'FAILED'; + this.verificationError.pan = 'PAN verification failed.'; + } + }, + error: (err: any) => { + this.verifying.pan = false; + this.verificationStatus.pan = 'FAILED'; + this.verificationError.pan = + err?.error?.developerMessage || + err?.error?.defaultUserMessage || + err?.message || + 'PAN verification failed. Please try again.'; + } + }); + } + + /** Show Aadhaar input in summary view for re-verification */ + startAadhaarReVerify(): void { + this.showAadhaarReVerify = true; + this.aadhaarFullNumber = ''; + this.verificationError.aadhaar = null; + } + + /** Step 1: Send OTP to Aadhaar-linked mobile */ + sendAadhaarOtp(): void { + const aadhaar = this.isEditMode ? this.kycForm?.get('aadhaarNumber')?.value : this.aadhaarFullNumber; + if (!aadhaar || aadhaar.length !== 12) { + this.verificationError.aadhaar = 'Please enter the full 12-digit Aadhaar number to generate OTP.'; + return; + } + if (!this.kycId) { + this.verificationError.aadhaar = 'KYC record not found. Save KYC details first.'; + return; + } + this.verifying.aadhaar = true; + this.verificationStatus.aadhaar = 'PENDING'; + this.verificationError.aadhaar = null; + this.aadhaarOtpSent = false; + this.aadhaarOtp = ''; + + this.clientsService + .sendAadhaarOtp(this.clientId, this.kycId, aadhaar) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: any) => { + this.verifying.aadhaar = false; + const refNumber = + response?.changes?.aadhaarRefNumber || + response?.aadhaarRefNumber || + response?.changes?.referenceNumber || + response?.resourceId; + this.aadhaarOtpSent = true; + this.aadhaarOtpReferenceId = refNumber || null; + this.verificationStatus.aadhaar = 'PENDING'; + }, + error: (err: any) => { + this.verifying.aadhaar = false; + this.verificationStatus.aadhaar = 'FAILED'; + this.verificationError.aadhaar = + err?.error?.developerMessage || + err?.error?.defaultUserMessage || + err?.message || + 'Failed to send OTP. Please try again.'; + } + }); + } + + /** Step 2: Verify OTP entered by user */ + verifyAadhaarOtp(): void { + if (!this.kycId || !this.aadhaarOtp || this.aadhaarOtp.length < 4) { + return; + } + this.aadhaarOtpVerifying = true; + this.verificationError.aadhaar = null; + + this.clientsService + .verifyAadhaarOtp(this.clientId, this.kycId, this.aadhaarOtp, this.aadhaarOtpReferenceId || '') + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: any) => { + this.aadhaarOtpVerifying = false; + if (response?.changes?.aadhaarVerified) { + this.verificationStatus.aadhaar = 'VERIFIED'; + this.verificationDetails.aadhaar = response.changes; + this.resetOtpState(); + this.loadKycData(); + } else { + this.verificationStatus.aadhaar = 'FAILED'; + this.verificationError.aadhaar = 'OTP verification failed.'; + this.resetOtpState(); + } + }, + error: (err: any) => { + this.aadhaarOtpVerifying = false; + this.verificationStatus.aadhaar = 'FAILED'; + this.verificationError.aadhaar = + err?.error?.developerMessage || + err?.error?.defaultUserMessage || + err?.message || + 'OTP verification failed. Please try again.'; + this.resetOtpState(); + } + }); + } + + verifyGSTIN(): void { + if (!this.kycId) { + this.verificationError.gstin = 'KYC record not found. Save KYC details first.'; + return; + } + this.verifying.gstin = true; + this.verificationStatus.gstin = 'PENDING'; + this.verificationError.gstin = null; + this.verificationDetails.gstin = null; + + this.clientsService + .verifyGSTIN(this.clientId, this.kycId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: any) => { + this.verifying.gstin = false; + if (response?.changes?.gstinVerified) { + this.verificationStatus.gstin = 'VERIFIED'; + this.verificationDetails.gstin = response.changes; + this.loadKycData(); + } else { + this.verificationStatus.gstin = 'FAILED'; + this.verificationError.gstin = 'GSTIN verification failed.'; + } + }, + error: (err: any) => { + this.verifying.gstin = false; + this.verificationStatus.gstin = 'FAILED'; + this.verificationError.gstin = + err?.error?.developerMessage || + err?.error?.defaultUserMessage || + err?.message || + 'GSTIN verification failed. Please try again.'; + } + }); + } + + formatDate(date: any): string { + if (!date) return '—'; + let year: number, month: number, day: number; + if (Array.isArray(date) && date.length >= 3) { + [ + year, + month, + day + ] = date; + } else { + return date; + } + const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + return `${day} ${months[month - 1]} ${year}`; + } + + private resetOtpState(): void { + this.aadhaarOtpSent = false; + this.aadhaarOtp = ''; + this.aadhaarOtpReferenceId = null; + this.aadhaarOtpVerifying = false; + this.showAadhaarReVerify = false; + this.aadhaarFullNumber = ''; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/src/app/clients/clients.module.ts b/src/app/clients/clients.module.ts index 2115234fd..5171c0131 100644 --- a/src/app/clients/clients.module.ts +++ b/src/app/clients/clients.module.ts @@ -65,6 +65,10 @@ import { ClientFamilyMemberDialogComponent } from './client-stepper/client-famil import { CaptureImageDialogComponent } from './clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component'; import { AddClientCollateralComponent } from './clients-view/client-actions/add-client-collateral/add-client-collateral.component'; import { ClientDatatableStepComponent } from './client-stepper/client-datatable-step/client-datatable-step.component'; +import { ClientKycStepComponent } from './client-stepper/client-kyc-step/client-kyc-step.component'; +import { KycTabComponent } from './clients-view/kyc-tab/kyc-tab.component'; +import { CkycTabComponent } from './clients-view/ckyc-tab/ckyc-tab.component'; +import { Form60TabComponent } from './clients-view/form60-tab/form60-tab.component'; /** * Clients Module @@ -125,7 +129,11 @@ import { ClientDatatableStepComponent } from './client-stepper/client-datatable- ClientFamilyMemberDialogComponent, CaptureImageDialogComponent, AddClientCollateralComponent, - ClientDatatableStepComponent + ClientDatatableStepComponent, + ClientKycStepComponent, + KycTabComponent, + CkycTabComponent, + Form60TabComponent ], providers: [] }) diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index 776f1c4f3..e672575cf 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -472,4 +472,89 @@ export class ClientsService { return this.externalHttp.post(apiUrl, { externalId }, { headers }); } + + /** + * Gets KYC details for a client. + * @param clientId Client ID + */ + getClientKyc(clientId: string): Observable { + return this.http.get(`/clients/${clientId}/kyc`); + } + + /** + * Saves KYC details for a client. + * @param clientId Client ID + * @param kycData KYC payload + */ + saveClientKyc(clientId: string, kycData: any): Observable { + return this.http.post(`/clients/${clientId}/kyc`, kycData); + } + + /** + * Updates existing KYC details for a client. + * @param clientId Client ID + * @param kycId KYC record ID + * @param kycData KYC payload + */ + updateClientKyc(clientId: string, kycId: string, kycData: any): Observable { + return this.http.put(`/clients/${clientId}/kyc/${kycId}`, kycData); + } + + /** + * Verifies PAN number via LincPay (reads PAN from the stored KYC record). + * @param clientId Client ID + * @param kycId KYC record ID + */ + verifyPAN(clientId: string, kycId: string): Observable { + return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-pan`, {}); + } + + /** + * Step 1 of Aadhaar e-KYC: sends OTP to the Aadhaar-linked mobile via LincPay. + * @param clientId Client ID + * @param kycId KYC record ID + * @param aadhaarNumber Full 12-digit Aadhaar number (never stored) + */ + sendAadhaarOtp(clientId: string, kycId: string, aadhaarNumber: string): Observable { + return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=aadhaar-generate-otp`, { aadhaarNumber }); + } + + /** + * Step 2 of Aadhaar e-KYC: verifies the OTP entered by the client via LincPay. + * @param clientId Client ID + * @param kycId KYC record ID + * @param otp OTP received on Aadhaar-linked mobile + * @param aadhaarRefNumber Reference number returned by sendAadhaarOtp (changes.aadhaarRefNumber) + */ + verifyAadhaarOtp(clientId: string, kycId: string, otp: string, aadhaarRefNumber: string): Observable { + return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-aadhaar`, { + aadhaarRefNumber, + aadhaarOtp: otp + }); + } + + /** + * Verifies GSTIN via LincPay (reads GSTIN from the stored KYC record). + * @param clientId Client ID + * @param kycId KYC record ID + */ + verifyGSTIN(clientId: string, kycId: string): Observable { + return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-gstin`, {}); + } + + getClientCkyc(clientId: string): Observable { + return this.http.get(`/clients/${clientId}/ckyc`); + } + + uploadCkycDocument(clientId: string, formData: FormData): Observable { + return this.http.post(`/clients/${clientId}/ckyc/documents`, formData); + } + + getClientForm60(clientId: string): Observable { + return this.http.get(`/clients/${clientId}/form60`); + } + + submitClientForm60(clientId: string, data: any): Observable { + return this.http.post(`/clients/${clientId}/form60`, data); + } } diff --git a/src/app/clients/create-client/create-client.component.html b/src/app/clients/create-client/create-client.component.html index cd61f30ba..02c7f2dcb 100644 --- a/src/app/clients/create-client/create-client.component.html +++ b/src/app/clients/create-client/create-client.component.html @@ -38,7 +38,7 @@ - {{ 'labels.inputs.FAMILY MEMBERS' | translate }} + {{ 'labels.inputs.Nominee' | translate }} + + {{ 'labels.inputs.KYC' | translate }} + + + @if (clientTemplate.isAddressEnabled) { {{ 'labels.inputs.ADDRESS' | translate }} diff --git a/src/app/clients/create-client/create-client.component.ts b/src/app/clients/create-client/create-client.component.ts index 9f1b73ad5..417a4f0f0 100644 --- a/src/app/clients/create-client/create-client.component.ts +++ b/src/app/clients/create-client/create-client.component.ts @@ -18,6 +18,7 @@ import { ClientGeneralStepComponent } from '../client-stepper/client-general-ste import { ClientFamilyMembersStepComponent } from '../client-stepper/client-family-members-step/client-family-members-step.component'; import { ClientAddressStepComponent } from '../client-stepper/client-address-step/client-address-step.component'; import { ClientDatatableStepComponent } from '../client-stepper/client-datatable-step/client-datatable-step.component'; +import { ClientKycStepComponent } from '../client-stepper/client-kyc-step/client-kyc-step.component'; /** Custom Services */ import { SettingsService } from 'app/settings/settings.service'; @@ -44,6 +45,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ClientFamilyMembersStepComponent, ClientAddressStepComponent, ClientDatatableStepComponent, + ClientKycStepComponent, ClientPreviewStepComponent ] }) @@ -59,6 +61,8 @@ export class CreateClientComponent { @ViewChild('clientFamily') clientFamilyMembersStep: ClientFamilyMembersStepComponent; /** Client Address Step */ @ViewChild('clientAddress') clientAddressStep: ClientAddressStepComponent; + /** Client KYC Step */ + @ViewChild('clientKyc') clientKycStep: ClientKycStepComponent; /** Get handle on dtclient tags in the template */ @ViewChildren('dtclient') clientDatatables: QueryList; @@ -167,13 +171,35 @@ export class CreateClientComponent { } this.clientsService.createClient(clientData).subscribe((response: any) => { - this.router.navigate( - [ - '../', - response.resourceId - ], - { relativeTo: this.route } - ); + const clientId = response.resourceId; + if (this.clientKycStep?.hasKycData) { + this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({ + complete: () => + this.router.navigate( + [ + '../', + clientId + ], + { relativeTo: this.route } + ), + error: () => + this.router.navigate( + [ + '../', + clientId + ], + { relativeTo: this.route } + ) + }); + } else { + this.router.navigate( + [ + '../', + clientId + ], + { relativeTo: this.route } + ); + } }); } } diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index 776d35ffa..f2a75c519 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -93,6 +93,19 @@
{{ 'labels.menus.Navigation' | translate }} + + + + + {{ 'labels.menus.CKYC' | translate }} + {{ 'labels.menus.Remittances' | translate }} + + + + + {{ 'labels.menus.Analytics' | translate }} + + + + + + {{ 'labels.menus.RBI Circulars' | translate }} + ; diff --git a/src/app/home/dashboard/dashboard.component.html b/src/app/home/dashboard/dashboard.component.html index b19728351..ccc16f8ee 100644 --- a/src/app/home/dashboard/dashboard.component.html +++ b/src/app/home/dashboard/dashboard.component.html @@ -6,11 +6,14 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- -