The demo-backup merge's git 3-way auto-resolve dropped several hunks without flagging conflicts, wherever demo had independently touched adjacent lines: the KYC Exception feature (client-kyc-step, kyc-tab, create-client, clients.service — checkbox, derivation logic, banners, contact-log endpoints), TDS report routes/menu links (reports-routing, shell), the Form 121 client tab (clients-routing, clients-view), the kyc-management exceptions dashboard, and the Form 121/15G-H register's real-data view integration. Restored by diffing demo-backup against dev per file and re-applying each genuine gap while preserving demo-backup's own independent work (Approval Center tab style, Loan Origination/Investment menus, form-97-98/risk-review report routes).
1215 lines
43 KiB
TypeScript
1215 lines
43 KiB
TypeScript
/**
|
|
* 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, QueryList, ViewChild, ViewChildren, inject } from '@angular/core';
|
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
import { Observable, of } from 'rxjs';
|
|
import { map } from 'rxjs/operators';
|
|
|
|
/** Custom Services */
|
|
import { ClientsService } from '../clients.service';
|
|
import { familyAllowed, nomineeAllowed, isProprietorship } from '../utils/client-rules';
|
|
|
|
/** Custom Components */
|
|
import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component';
|
|
import { ClientAddressStepComponent } from '../client-stepper/client-address-step/client-address-step.component';
|
|
import { ClientIndiaAddressStepComponent } from '../client-stepper/client-india-address-step/client-india-address-step.component';
|
|
import { ClientEntityKycStepComponent } from '../client-stepper/client-entity-kyc-step/client-entity-kyc-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';
|
|
import { ClientPreviewStepComponent } from '../client-stepper/client-preview-step/client-preview-step.component';
|
|
import { ClientFamilyMembersStepComponent } from '../client-stepper/client-family-members-step/client-family-members-step.component';
|
|
|
|
/** Custom Services */
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
import { MatIconModule, MatIcon } from '@angular/material/icon';
|
|
import { MatDivider } from '@angular/material/divider';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
import { MatDialog, MAT_DIALOG_DATA, MatDialogClose } from '@angular/material/dialog';
|
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
import { MatTooltip } from '@angular/material/tooltip';
|
|
import { Inject } from '@angular/core';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
|
|
/**
|
|
* Create Client Component.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-create-client',
|
|
templateUrl: './create-client.component.html',
|
|
styleUrls: ['./create-client.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatTabsModule,
|
|
MatIconModule,
|
|
MatDivider,
|
|
FaIconComponent,
|
|
MatProgressSpinner,
|
|
ClientGeneralStepComponent,
|
|
ClientAddressStepComponent,
|
|
ClientIndiaAddressStepComponent,
|
|
ClientEntityKycStepComponent,
|
|
ClientDatatableStepComponent,
|
|
ClientKycStepComponent,
|
|
ClientPreviewStepComponent,
|
|
ClientFamilyMembersStepComponent,
|
|
MatProgressBarModule,
|
|
MatTooltip
|
|
]
|
|
})
|
|
export class CreateClientComponent {
|
|
private route = inject(ActivatedRoute);
|
|
private router = inject(Router);
|
|
private clientsService = inject(ClientsService);
|
|
private settingsService = inject(SettingsService);
|
|
private dialog = inject(MatDialog);
|
|
private snackBar = inject(MatSnackBar);
|
|
|
|
@ViewChild(ClientGeneralStepComponent) clientGeneralStep: ClientGeneralStepComponent;
|
|
@ViewChild('clientAddress') clientAddressStep: ClientAddressStepComponent;
|
|
@ViewChild('clientIndiaAddress') clientIndiaAddressStep: ClientIndiaAddressStepComponent;
|
|
@ViewChild(ClientEntityKycStepComponent) clientEntityKycStep: ClientEntityKycStepComponent;
|
|
@ViewChild('clientKyc') clientKycStep: ClientKycStepComponent;
|
|
@ViewChild(ClientFamilyMembersStepComponent) clientFamilyMembersStep: ClientFamilyMembersStepComponent;
|
|
@ViewChildren('dtclient') clientDatatables: QueryList<ClientDatatableStepComponent>;
|
|
|
|
datatables: any = [];
|
|
legalFormType = 1;
|
|
selectedTabIndex = 0;
|
|
createdClientId = '';
|
|
savingClient = false;
|
|
/** Pre-fill data passed from "Create as Client" on a manual family member */
|
|
prefillData: any = null;
|
|
|
|
/** Track which tabs are in edit mode (false/undefined = read-only for completed tabs) */
|
|
tabEditMode: Record<number, boolean> = {};
|
|
/** Highest tab index reached — tabs below this show read-only when navigated back */
|
|
highestTabReached = 0;
|
|
|
|
/** Client Template */
|
|
clientTemplate: any;
|
|
/** Client Address Field Config */
|
|
clientAddressFieldConfig: any;
|
|
|
|
/**
|
|
* Fetches client and address template from `resolve`
|
|
* @param {ActivatedRoute} route Activated Route
|
|
* @param {Router} router Router
|
|
* @param {ClientsService} clientsService Clients Service
|
|
* @param {SettingsService} settingsService Setting service
|
|
*/
|
|
constructor() {
|
|
this.route.data.subscribe((data: { clientTemplate: any; clientAddressFieldConfig: any }) => {
|
|
this.clientTemplate = data.clientTemplate;
|
|
this.clientAddressFieldConfig = data.clientAddressFieldConfig;
|
|
// This app's RouteReusableStrategy keeps this SAME component instance alive across
|
|
// repeat visits to /clients/create, so ngOnInit/constructor won't re-run for a second
|
|
// "create new client" session — reset transient state here instead, since `route.data`
|
|
// re-emits on every navigation regardless of whether the component was reused. Must run
|
|
// BEFORE setDatatables(), which reads legalFormType to filter clientTemplate.datatables.
|
|
this.resetForNewClient();
|
|
this.setDatatables();
|
|
});
|
|
// Read pre-fill data set by "Create as Client" from a manual family member
|
|
const nav = this.router.getCurrentNavigation();
|
|
const navState = nav?.extras?.state ?? (typeof history !== 'undefined' ? history.state : null);
|
|
if (navState?.prefill) {
|
|
this.prefillData = navState.prefill;
|
|
}
|
|
}
|
|
|
|
/** Resets session state that could otherwise leak between clients when the route is reused. */
|
|
private resetForNewClient(): void {
|
|
this.legalFormType = 1;
|
|
this.selectedTabIndex = 0;
|
|
this.createdClientId = '';
|
|
this.savingClient = false;
|
|
this.saveProgress = 0;
|
|
this.tabEditMode = {};
|
|
this.highestTabReached = 0;
|
|
this.validationError = '';
|
|
this.prefillData = null;
|
|
this.clientGeneralStep?.resetForNewClient();
|
|
this.clientKycStep?.resetForNewClient();
|
|
this.clientIndiaAddressStep?.resetForNewClient();
|
|
}
|
|
|
|
/** Check if a tab is in read-only mode (completed tab navigated back to) */
|
|
isTabReadOnly(tabIndex: number): boolean {
|
|
// Only tabs that have been passed (below highest reached) AND not explicitly set to edit mode
|
|
return tabIndex < this.highestTabReached && !this.tabEditMode[tabIndex];
|
|
}
|
|
|
|
/** Enable editing for a specific tab */
|
|
enableTabEdit(tabIndex: number): void {
|
|
this.tabEditMode[tabIndex] = true;
|
|
}
|
|
|
|
/** Cancel editing — revert tab back to read-only */
|
|
cancelTabEdit(tabIndex: number): void {
|
|
this.tabEditMode[tabIndex] = false;
|
|
}
|
|
|
|
get clientGeneralForm() {
|
|
return this.clientGeneralStep?.createClientForm;
|
|
}
|
|
|
|
/**
|
|
* Constitution display name for the selected entity constitution. Drives the
|
|
* proprietorship-only nominee rule below. Empty / null for individual clients.
|
|
*/
|
|
get constitutionLabel(): string {
|
|
return this.clientGeneralStep?.selectedConstitution?.name || '';
|
|
}
|
|
|
|
/** Whether the Family/Nominee tab should appear at all. */
|
|
get showFamilyTab(): boolean {
|
|
return nomineeAllowed(this.legalFormType, this.constitutionLabel);
|
|
}
|
|
|
|
/**
|
|
* Whether the Family/Nominee tab should render in nominee-only mode (family
|
|
* member rows hidden). True for entity proprietorship, false for individuals.
|
|
*/
|
|
get familyTabNomineeOnly(): boolean {
|
|
return !familyAllowed(this.legalFormType, this.constitutionLabel) && this.showFamilyTab;
|
|
}
|
|
|
|
/** Total tab count varies by legal form + constitution.
|
|
* Individual: General + KYC + Address + Family + Preview = 5
|
|
* Entity (proprietorship): General + KYC + S&S + Address + Nominees + Preview = 6
|
|
* Entity (non-proprietor): General + KYC + S&S + Address + Preview = 5
|
|
*/
|
|
get totalTabs(): number {
|
|
let count = 4; // General + KYC + Address + Preview
|
|
if (this.legalFormType === 2) count++; // S&S only for entities
|
|
if (this.showFamilyTab) count++;
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Per-tab form-validity gate for the Save & Next button.
|
|
* Returns false when the current tab has unfilled / invalid required fields
|
|
* so the user can't move forward (and the button is visibly disabled).
|
|
* The mapping follows the tab order in create-client.component.html:
|
|
* 0 General
|
|
* 1 KYC & Documents
|
|
* 2 (non-individual) Scheme & Sector | (individual) Address
|
|
* 3 (non-individual) Address | (individual) Family
|
|
* 4 Application Summary (handled by Submit, not Save & Next)
|
|
*/
|
|
get currentStepValid(): boolean {
|
|
if (this.savingClient) return false;
|
|
const idx = this.selectedTabIndex;
|
|
|
|
if (idx === 0) {
|
|
if (!this.clientGeneralStep || !this.clientGeneralForm) return false;
|
|
if (this.clientGeneralStep.sanctionsBlocked) return false;
|
|
if (!this.clientGeneralForm.valid) return false;
|
|
// Contact-OTP gate: mobileNo/emailAddress must each be OTP-verified before a brand-new
|
|
// client can be created — mirrors the edit flow's dual-OTP gate in EditClientComponent.
|
|
return this.clientGeneralStep.contactVerified;
|
|
}
|
|
if (idx === 1) {
|
|
// KYC tab — block when the kyc form has unfilled required fields
|
|
// (kycDocumentName / kycDocumentNumber / pattern errors on PAN / Aadhaar etc.)
|
|
const kf = this.clientKycStep?.kycForm;
|
|
return !kf || kf.valid;
|
|
}
|
|
if (this.legalFormType === 2) {
|
|
if (idx === 2) {
|
|
// Scheme & Sector (entity KYC) — categories are opt-in, so always allow.
|
|
return true;
|
|
}
|
|
if (idx === 3) {
|
|
// Address tab for non-individuals
|
|
return this.isAddressStepValid();
|
|
}
|
|
} else {
|
|
if (idx === 2) {
|
|
// Address tab for individuals
|
|
return this.isAddressStepValid();
|
|
}
|
|
if (idx === 3) {
|
|
// Family tab — Mother is required, and either Father or a Caretaker.
|
|
return !this.clientFamilyMembersStep || this.clientFamilyMembersStep.familyRelationshipRulesMet;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Permanent address is the only mandatory address; current/office/correspondence are optional. */
|
|
private isAddressStepValid(): boolean {
|
|
const step = this.clientIndiaAddressStep;
|
|
if (!step) return true; // step not yet mounted — don't block
|
|
const perm = step.permanentForm;
|
|
return !perm || perm.valid;
|
|
}
|
|
|
|
/**
|
|
* Human-readable explanation of why the Save & Next button is disabled.
|
|
* Surfaced via matTooltip so the user knows what to fix.
|
|
*/
|
|
get currentStepBlockerMessage(): string {
|
|
if (this.savingClient) return 'Saving — please wait…';
|
|
if (this.currentStepValid) return '';
|
|
const idx = this.selectedTabIndex;
|
|
if (idx === 0 && this.clientGeneralStep?.sanctionsBlocked) {
|
|
return 'Client failed sanctions screening — cannot proceed.';
|
|
}
|
|
if (idx === 0 && this.clientGeneralForm?.valid && !this.clientGeneralStep?.contactVerified) {
|
|
return 'Verify the OTP sent to both the mobile number and email address before proceeding.';
|
|
}
|
|
if (idx === 1) {
|
|
const labels = this.clientKycStep?.invalidFieldLabels || [];
|
|
if (labels.length) {
|
|
return `Please fill/correct: ${labels.join(', ')}.`;
|
|
}
|
|
}
|
|
if (idx === 3 && this.legalFormType === 1 && this.clientFamilyMembersStep) {
|
|
const step = this.clientFamilyMembersStep;
|
|
if (!step.hasMother) return 'Mother is required as a family member.';
|
|
if (!step.hasFather && !step.hasCaretaker) return 'Either Father or a Caretaker is required as a family member.';
|
|
}
|
|
return 'Please fill all mandatory fields on this tab to continue.';
|
|
}
|
|
|
|
/** Get constitution type from general step for entity KYC */
|
|
getConstitutionType(): string {
|
|
return this.clientGeneralStep?.createClientForm?.get('clientNonPersonDetails.constitutionId')?.value || '';
|
|
}
|
|
|
|
get client() {
|
|
const general = this.clientGeneralStep?.clientGeneralDetails || {};
|
|
const addr = this.clientTemplate.isAddressEnabled && this.clientAddressStep ? this.clientAddressStep.address : {};
|
|
return { ...general, ...addr };
|
|
}
|
|
|
|
areFormvalids(): boolean {
|
|
if (!this.clientGeneralStep || !this.clientGeneralForm) {
|
|
return false;
|
|
}
|
|
let areValids = this.clientGeneralForm.valid;
|
|
if (this.clientGeneralStep.sanctionsBlocked) {
|
|
return false;
|
|
}
|
|
if (this.clientTemplate.isAddressEnabled && this.clientAddressStep) {
|
|
areValids = areValids && this.clientAddressStep.address.address.length > 0;
|
|
}
|
|
if (this.clientTemplate.datatables && this.clientTemplate.datatables.length > 0 && this.clientDatatables) {
|
|
this.clientDatatables.forEach((clientDatatable: ClientDatatableStepComponent) => {
|
|
areValids = areValids && clientDatatable.datatableForm.valid;
|
|
});
|
|
}
|
|
return areValids;
|
|
}
|
|
|
|
setDatatables(): void {
|
|
this.datatables = [];
|
|
let legalFormTypeVal = 'person';
|
|
if (this.legalFormType === 2) {
|
|
legalFormTypeVal = 'entity';
|
|
}
|
|
if (this.clientTemplate.datatables) {
|
|
this.clientTemplate.datatables.forEach((datatable: any) => {
|
|
if (datatable.entitySubType.toLowerCase() === legalFormTypeVal) {
|
|
this.datatables.push(datatable);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Next button handler — saves client on General tab before moving forward.
|
|
*/
|
|
/** Save progress percentage for progress bar */
|
|
saveProgress = 0;
|
|
|
|
onNext() {
|
|
// If on General tab and client not yet created — save first
|
|
if (this.selectedTabIndex === 0 && !this.createdClientId) {
|
|
if (!this.clientGeneralForm?.valid) {
|
|
this.clientGeneralForm?.markAllAsTouched();
|
|
setTimeout(() => {
|
|
const firstInvalid = document.querySelector('.mat-mdc-form-field.ng-invalid');
|
|
if (firstInvalid) {
|
|
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
// Contact-OTP gate — defensive re-check. The Save & Next button is already disabled via
|
|
// `currentStepValid` while this is incomplete, but don't rely on that alone (mirrors the
|
|
// same defensive re-check in EditClientComponent.confirmAndSaveTab()).
|
|
if (!this.clientGeneralStep?.contactVerified) {
|
|
this.snackBar.open('Verify the OTP sent to both the mobile number and email address before saving.', 'Close', {
|
|
duration: 5000
|
|
});
|
|
return;
|
|
}
|
|
|
|
// PWD gate — disability type + certificate are mandatory when PWD is checked
|
|
// (Senior Citizen proof upload is optional, not gated here)
|
|
if (!this.clientGeneralStep?.seniorPwdDocsComplete) {
|
|
this.clientGeneralStep?.markSeniorPwdDocsTouched();
|
|
this.snackBar.open(
|
|
'Select the disability type and upload the Disability Certificate before continuing.',
|
|
'Close',
|
|
{ duration: 5000 }
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Sanctions verification gate — Verify Client must be clicked before save
|
|
if (!this.clientGeneralStep?.sanctionsVerified) {
|
|
this.dialog
|
|
.open(VerifyClientAlertDialogComponent, {
|
|
width: '420px',
|
|
autoFocus: false
|
|
})
|
|
.afterClosed()
|
|
.subscribe(() => {
|
|
setTimeout(() => {
|
|
const verifyBtn = document.querySelector('.verify-name-btn') as HTMLElement;
|
|
if (verifyBtn) {
|
|
verifyBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
verifyBtn.focus();
|
|
}
|
|
}, 100);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Show confirmation dialog
|
|
const dialogRef = this.dialog.open(SaveConfirmDialogComponent, {
|
|
width: '440px',
|
|
disableClose: true
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe((confirmed: boolean) => {
|
|
if (!confirmed) return;
|
|
this.saveAndProceed();
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Save current tab data before moving to next
|
|
if (this.createdClientId) {
|
|
this.saveCurrentTabData();
|
|
}
|
|
|
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
|
if (this.selectedTabIndex > this.highestTabReached) {
|
|
this.highestTabReached = this.selectedTabIndex;
|
|
}
|
|
}
|
|
|
|
/** Save the current tab's data to the API */
|
|
private saveCurrentTabData() {
|
|
const tabIndex = this.selectedTabIndex;
|
|
const clientId = this.createdClientId;
|
|
|
|
// KYC tab
|
|
const kycTabIndex = 1;
|
|
if (tabIndex === kycTabIndex) {
|
|
// Photograph + Signature are mandatory before leaving the KYC tab
|
|
if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
|
|
this.clientKycStep.markPhotoSigTouched();
|
|
this.validationError = 'Photograph and Signature are required before continuing.';
|
|
return;
|
|
}
|
|
this.validationError = '';
|
|
// Save KYC
|
|
if (this.clientKycStep?.hasKycData) {
|
|
if (!this.clientKycStep.kycId) {
|
|
this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({
|
|
next: (res: any) => {
|
|
if (this.clientKycStep) {
|
|
this.clientKycStep.kycId = res?.resourceId?.toString() || res?.entityId?.toString() || '';
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
} else {
|
|
const { kycType, ...updatePayload } = this.clientKycStep.kycData;
|
|
this.clientsService
|
|
.updateClientKyc(clientId, this.clientKycStep.kycId, updatePayload)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
}
|
|
// Upload photo & signature
|
|
if (this.clientKycStep?.photographFile) {
|
|
this.clientsService
|
|
.uploadClientProfileImage(clientId, this.clientKycStep.photographFile)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
if (this.clientKycStep?.signatureFile) {
|
|
this.clientsService
|
|
.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
// Persist the KYC document picker selection + uploaded files as a client identifier
|
|
this.saveKycDocProof(clientId);
|
|
return;
|
|
}
|
|
|
|
// Address tab (index 2 for Individual, 3 for Non-Individual)
|
|
const addressTabIndex = this.legalFormType === 2 ? 3 : 2;
|
|
if (tabIndex === addressTabIndex) {
|
|
if (this.clientIndiaAddressStep?.hasAddressData) {
|
|
this.saveClientAddresses(clientId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Family tab (index 3 for Individual, 4 for Non-Individual proprietorship).
|
|
// Skip entirely when the tab is hidden (entity non-proprietor) so we don't
|
|
// confuse Preview at the same index slot with the Family step.
|
|
const familyTabIndex = this.legalFormType === 2 ? 4 : 3;
|
|
if (this.showFamilyTab && tabIndex === familyTabIndex) {
|
|
if (this.clientFamilyMembersStep?.hasFamilyMembers) {
|
|
this.saveClientFamilyMembers(clientId);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/** Executes the save API call with progress animation */
|
|
private saveAndProceed() {
|
|
this.savingClient = true;
|
|
this.saveProgress = 0;
|
|
|
|
// Animate progress bar
|
|
const progressInterval = setInterval(() => {
|
|
if (this.saveProgress < 90) {
|
|
this.saveProgress += Math.random() * 15;
|
|
if (this.saveProgress > 90) this.saveProgress = 90;
|
|
}
|
|
}, 200);
|
|
|
|
const locale = this.settingsService.language.code;
|
|
const dateFormat = this.settingsService.dateFormat;
|
|
const clientData = { ...this.client, dateFormat, locale };
|
|
|
|
// Mandatory contact-OTP references — required by POST /clients whenever mobileNo/
|
|
// emailAddress are present (they always are, both are Validators.required on this form).
|
|
// Gated by `contactVerified` above, so these are guaranteed non-null at this point.
|
|
clientData.mobileNewOtpId = this.clientGeneralStep?.mobileNewOtpId;
|
|
clientData.emailNewOtpId = this.clientGeneralStep?.emailNewOtpId;
|
|
|
|
if (this.clientTemplate.datatables && this.clientTemplate.datatables.length > 0) {
|
|
const datatables: any[] = [];
|
|
this.clientDatatables?.forEach((clientDatatable: ClientDatatableStepComponent) => {
|
|
datatables.push(clientDatatable.payload);
|
|
});
|
|
if (datatables.length > 0) {
|
|
clientData['datatables'] = datatables;
|
|
}
|
|
}
|
|
|
|
this.clientsService.createClient(clientData).subscribe({
|
|
next: (response: any) => {
|
|
clearInterval(progressInterval);
|
|
this.saveProgress = 100;
|
|
this.createdClientId = response.resourceId;
|
|
|
|
// Senior Citizen / PWD supporting documents — uploaded after creation, same pattern as
|
|
// the KYC step's photo/signature uploads (the generic /clients/{id}/documents endpoint
|
|
// needs a real clientId, which only exists once createClient() has returned).
|
|
if (this.clientGeneralStep?.seniorCitizenProofFile) {
|
|
const seniorCitizenProofForm = new FormData();
|
|
seniorCitizenProofForm.append('file', this.clientGeneralStep.seniorCitizenProofFile);
|
|
seniorCitizenProofForm.append('name', 'seniorCitizenProof');
|
|
seniorCitizenProofForm.append('description', 'Senior Citizen Proof (Age/ID Proof)');
|
|
this.clientsService
|
|
.uploadClientDocument(this.createdClientId, seniorCitizenProofForm)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
if (this.clientGeneralStep?.pwdCertificateFile) {
|
|
const pwdCertificateForm = new FormData();
|
|
pwdCertificateForm.append('file', this.clientGeneralStep.pwdCertificateFile);
|
|
pwdCertificateForm.append('name', 'pwdCertificate');
|
|
pwdCertificateForm.append('description', 'Disability Certificate');
|
|
this.clientsService
|
|
.uploadClientDocument(this.createdClientId, pwdCertificateForm)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
|
|
// Brief delay to show 100% before transitioning
|
|
setTimeout(() => {
|
|
this.savingClient = false;
|
|
this.saveProgress = 0;
|
|
this.selectedTabIndex = 1;
|
|
this.highestTabReached = 1;
|
|
}, 400);
|
|
},
|
|
error: () => {
|
|
clearInterval(progressInterval);
|
|
this.savingClient = false;
|
|
this.saveProgress = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
legalFormChange(eventData: { legalForm: number }) {
|
|
this.legalFormType = eventData.legalForm;
|
|
this.setDatatables();
|
|
}
|
|
|
|
/**
|
|
* Submits the create client form.
|
|
* If the client was already created via onNext(), reuse that client ID
|
|
* instead of creating a duplicate.
|
|
*/
|
|
/** Validation error message for display */
|
|
validationError = '';
|
|
|
|
submit() {
|
|
// Validate: photograph and signature are mandatory
|
|
if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
|
|
this.clientKycStep.markPhotoSigTouched();
|
|
this.validationError = 'Photograph and Signature are required before submitting.';
|
|
// Switch to KYC tab
|
|
this.selectedTabIndex = 1;
|
|
return;
|
|
}
|
|
// Validate: at least one family member required (Individual clients)
|
|
if (this.legalFormType === 1 && this.clientFamilyMembersStep && !this.clientFamilyMembersStep.hasFamilyMembers) {
|
|
this.validationError = 'At least one family member is required. Please add a family member before submitting.';
|
|
// Switch to Family tab
|
|
this.selectedTabIndex = 3;
|
|
return;
|
|
}
|
|
// Validate: Mother required, and either Father or a Caretaker (Individual clients)
|
|
if (
|
|
this.legalFormType === 1 &&
|
|
this.clientFamilyMembersStep &&
|
|
!this.clientFamilyMembersStep.familyRelationshipRulesMet
|
|
) {
|
|
const step = this.clientFamilyMembersStep;
|
|
if (!step.hasMother) {
|
|
this.validationError = 'Mother is required as a family member.';
|
|
} else {
|
|
this.validationError = 'Either Father or a Caretaker is required as a family member.';
|
|
}
|
|
this.selectedTabIndex = 3;
|
|
return;
|
|
}
|
|
this.validationError = '';
|
|
|
|
if (this.createdClientId) {
|
|
// Client already created in onNext() — just finalize (upload files, navigate)
|
|
this.finalizeClient(this.createdClientId);
|
|
} else {
|
|
// Fallback: create client now (shouldn't happen in normal flow)
|
|
if (!this.clientGeneralStep?.contactVerified) {
|
|
this.validationError = 'Verify the OTP sent to both the mobile number and email address before submitting.';
|
|
this.selectedTabIndex = 0;
|
|
return;
|
|
}
|
|
const locale = this.settingsService.language.code;
|
|
const dateFormat = this.settingsService.dateFormat;
|
|
const clientData = {
|
|
...this.client,
|
|
dateFormat,
|
|
locale,
|
|
mobileNewOtpId: this.clientGeneralStep?.mobileNewOtpId,
|
|
emailNewOtpId: this.clientGeneralStep?.emailNewOtpId
|
|
};
|
|
|
|
if (this.clientTemplate.datatables && this.clientTemplate.datatables.length > 0) {
|
|
const datatables: any[] = [];
|
|
this.clientDatatables.forEach((clientDatatable: ClientDatatableStepComponent) => {
|
|
datatables.push(clientDatatable.payload);
|
|
});
|
|
if (datatables.length > 0) {
|
|
clientData['datatables'] = datatables;
|
|
}
|
|
}
|
|
|
|
this.clientsService.createClient(clientData).subscribe((response: any) => {
|
|
this.createdClientId = response.resourceId;
|
|
this.finalizeClient(this.createdClientId);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finalize: upload photo/signature, save KYC, save addresses, then navigate.
|
|
*/
|
|
private finalizeClient(clientId: string) {
|
|
const doNavigate = () =>
|
|
this.router.navigate(
|
|
[
|
|
'../',
|
|
clientId
|
|
],
|
|
{ relativeTo: this.route }
|
|
);
|
|
|
|
const navigateToClient = () => {
|
|
const fm = this.prefillData;
|
|
if (fm?.familyMemberId && fm?.parentClientId) {
|
|
this.clientsService
|
|
.editFamilyMember(fm.parentClientId.toString(), fm.familyMemberId.toString(), {
|
|
linkedClientId: Number(clientId)
|
|
})
|
|
.subscribe({
|
|
next: () => {
|
|
this.snackBar.open('Family member linked to this client successfully.', 'Close', { duration: 4000 });
|
|
doNavigate();
|
|
},
|
|
error: () => {
|
|
this.snackBar.open(
|
|
'Client created, but family member could not be auto-linked. Please link manually.',
|
|
'Close',
|
|
{ duration: 6000 }
|
|
);
|
|
doNavigate();
|
|
}
|
|
});
|
|
} else {
|
|
doNavigate();
|
|
}
|
|
};
|
|
|
|
// Upload photo if selected
|
|
if (this.clientKycStep?.photographFile) {
|
|
this.clientsService
|
|
.uploadClientProfileImage(clientId, this.clientKycStep.photographFile)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
|
|
// Upload signature if selected
|
|
if (this.clientKycStep?.signatureFile) {
|
|
this.clientsService
|
|
.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile)
|
|
.subscribe({ error: () => {} });
|
|
}
|
|
|
|
// Save addresses from India address step
|
|
if (this.clientIndiaAddressStep?.hasAddressData) {
|
|
this.saveClientAddresses(clientId);
|
|
}
|
|
|
|
// Save family members
|
|
if (this.clientFamilyMembersStep?.hasFamilyMembers) {
|
|
this.saveClientFamilyMembers(clientId);
|
|
}
|
|
|
|
// Persist the KYC document picker selection + uploaded files as a client identifier
|
|
this.saveKycDocProof(clientId);
|
|
|
|
// Save/update KYC to ensure latest exception fields are persisted
|
|
if (this.clientKycStep?.hasKycData) {
|
|
if (!this.clientKycStep.kycId) {
|
|
this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({
|
|
next: () => navigateToClient(),
|
|
error: () => navigateToClient()
|
|
});
|
|
} else {
|
|
const { kycType, ...updatePayload } = this.clientKycStep.kycData;
|
|
this.clientsService.updateClientKyc(clientId, this.clientKycStep.kycId, updatePayload).subscribe({
|
|
next: () => navigateToClient(),
|
|
error: () => navigateToClient()
|
|
});
|
|
}
|
|
} else {
|
|
navigateToClient();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save addresses from the India address step to Fineract address API.
|
|
* Fetches address template to resolve address type IDs, then POSTs each address.
|
|
*/
|
|
private saveClientAddresses(clientId: string) {
|
|
const addressData = this.clientIndiaAddressStep.addressData;
|
|
// Map tab names to Fineract address type names (as defined in address template)
|
|
const typeNameMap: Record<string, string[]> = {
|
|
permanent: [
|
|
'Permanent',
|
|
'Permanent Address'
|
|
],
|
|
current: [
|
|
'Residential',
|
|
'Current',
|
|
'Current / Residential',
|
|
'Communication'
|
|
],
|
|
office: [
|
|
'Office',
|
|
'Office / Work',
|
|
'Business',
|
|
'Work'
|
|
],
|
|
correspondence: [
|
|
'Correspondence',
|
|
'Mailing',
|
|
'Postal'
|
|
]
|
|
};
|
|
|
|
this.clientsService.getClientAddressTemplate().subscribe({
|
|
next: (template: any) => {
|
|
const typeOptions = template?.addressTypeIdOptions || [];
|
|
for (const [
|
|
tab,
|
|
addr
|
|
] of Object.entries(addressData)) {
|
|
if (!addr || !(addr as any).addressLine1) continue;
|
|
|
|
// Find matching address type ID
|
|
const possibleNames = typeNameMap[tab] || [tab];
|
|
let typeId = typeOptions.find((t: any) =>
|
|
possibleNames.some((n: string) => t.name?.toLowerCase().includes(n.toLowerCase()))
|
|
)?.id;
|
|
|
|
// Fallback: use first available type if no match
|
|
if (!typeId && typeOptions.length > 0) {
|
|
typeId = typeOptions[0].id;
|
|
}
|
|
if (!typeId) continue;
|
|
|
|
const payload: any = {
|
|
addressLine1: (addr as any).addressLine1 || '',
|
|
addressLine2: (addr as any).addressLine2 || '',
|
|
addressLine3: '',
|
|
townVillage: (addr as any).locality || '',
|
|
postalCode: (addr as any).pinCode || '',
|
|
city: (addr as any).city || '',
|
|
countyDistrict: (addr as any).district || '',
|
|
street: (addr as any).landmark || '',
|
|
addressType: typeId,
|
|
isActive: tab === 'permanent'
|
|
};
|
|
|
|
// Try to resolve state to stateProvinceId. We normalise both sides (lowercase,
|
|
// collapse underscores + whitespace) so legacy UPPER_SNAKE rows still match the
|
|
// postal-PIN API's "Madhya Pradesh" form and vice versa. See migration 0297.
|
|
if ((addr as any).state && template?.stateProvinceIdOptions?.length) {
|
|
const norm = (s: string) =>
|
|
(s || '')
|
|
.toLowerCase()
|
|
.replace(/[_\s]+/g, ' ')
|
|
.trim();
|
|
const target = norm((addr as any).state);
|
|
const stateMatch = template.stateProvinceIdOptions.find((s: any) => norm(s.name) === target);
|
|
if (stateMatch) payload.stateProvinceId = stateMatch.id;
|
|
}
|
|
|
|
this.clientsService.createClientAddress(clientId, typeId.toString(), payload).subscribe({ error: () => {} });
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
|
|
// Persist the selected proof of address as a client identifier (with attached files)
|
|
this.saveAddressProof(clientId);
|
|
}
|
|
|
|
/**
|
|
* Save the address-proof picked on the permanent tab as a client identifier
|
|
* + upload each selected file as a document attached to that identifier.
|
|
* Mirrors identities-tab.createIdentifierWithDocs.
|
|
*/
|
|
private saveAddressProof(clientId: string) {
|
|
const payload = this.clientIndiaAddressStep?.addressProofPayload;
|
|
if (!payload?.proofId) return;
|
|
|
|
const proofName = payload.proof?.name || 'Address Proof';
|
|
const identifierData: any = {
|
|
documentTypeId: payload.proofId,
|
|
status: 'Active',
|
|
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
|
description: payload.issueDate ? `Issue date: ${payload.issueDate}` : ''
|
|
};
|
|
|
|
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
|
|
next: (res: any) => {
|
|
const identifierId = res?.resourceId;
|
|
if (!identifierId) return;
|
|
for (const entry of payload.files) {
|
|
const formData = new FormData();
|
|
formData.append('name', `${proofName} — ${entry.slot}`);
|
|
formData.append('file', entry.file);
|
|
this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save the KYC document picked on the KYC tab as a client identifier
|
|
* + upload each selected file as a document attached to that identifier.
|
|
*/
|
|
private saveKycDocProof(clientId: string) {
|
|
const payload = this.clientKycStep?.kycDocPayload;
|
|
if (!payload?.proofId || !payload?.files?.length) return;
|
|
|
|
const proofName = payload.proof?.name || 'KYC Document';
|
|
// The KYC step's chip-picker carries `id` as a doc-type CODE string (e.g. "AADHAAR")
|
|
// because its list is constitution-driven, not CodeValue-backed. The backend
|
|
// ClientIdentifierRequest.documentTypeId is a Long, so a string proofId trips Jackson
|
|
// with HttpMessageNotReadableException → 400. Resolve to the numeric CodeValue id by
|
|
// matching on name against /kyc/proof-types?type=id before posting.
|
|
this.resolveDocumentTypeId(payload.proofId, proofName).subscribe((documentTypeId: number | null) => {
|
|
if (documentTypeId == null) {
|
|
// No CodeValue match (e.g. non-individual constitution-specific docs). Skip the
|
|
// identifier save rather than firing a 400; the form's other KYC fields still get
|
|
// persisted via the KYC entity.
|
|
return;
|
|
}
|
|
const identifierData: any = {
|
|
documentTypeId,
|
|
status: 'Active',
|
|
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
|
description: 'KYC document'
|
|
};
|
|
|
|
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
|
|
next: (res: any) => {
|
|
const identifierId = res?.resourceId;
|
|
if (!identifierId) return;
|
|
for (const entry of payload.files) {
|
|
const formData = new FormData();
|
|
formData.append('name', `${proofName} — ${entry.slot}`);
|
|
formData.append('file', entry.file);
|
|
this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pass-through when proofId is already numeric (address proof, identities tab). Otherwise
|
|
* resolve the string code/name against the ID-proof CodeValues so the backend gets a
|
|
* proper Long for documentTypeId.
|
|
*/
|
|
private resolveDocumentTypeId(proofId: any, proofName: string): Observable<number | null> {
|
|
if (typeof proofId === 'number' && Number.isFinite(proofId)) {
|
|
return of(proofId);
|
|
}
|
|
return this.clientsService.getKycProofTypes('id').pipe(
|
|
map((res: any) => {
|
|
const list: any[] = res?.idProofs || [];
|
|
const targetName = (proofName || '').trim().toLowerCase();
|
|
const match = list.find((p: any) => (p?.name || '').trim().toLowerCase() === targetName);
|
|
return (match?.id ?? null) as number | null;
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Save family members via POST /clients/{id}/familymembers
|
|
*/
|
|
private saveClientFamilyMembers(clientId: string) {
|
|
const members = this.clientFamilyMembersStep.clientFamilyMembers;
|
|
for (const member of members) {
|
|
const marriageCertFile = member._marriageCertFile || null;
|
|
const payload = { ...member };
|
|
// Remove internal/transient fields
|
|
delete payload._linkedClient;
|
|
delete payload._marriageCertFile;
|
|
delete payload.dateFormat;
|
|
delete payload.locale;
|
|
payload.dateFormat = this.settingsService.dateFormat;
|
|
payload.locale = this.settingsService.language.code;
|
|
this.clientsService.addFamilyMember(clientId, payload).subscribe({
|
|
next: (res: any) => {
|
|
// Upload marriage certificate if present
|
|
if (marriageCertFile && res?.resourceId) {
|
|
this.uploadFamilyMemberDocument(clientId, res.resourceId, marriageCertFile, 'Marriage Certificate');
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload a document for a family member.
|
|
*/
|
|
private uploadFamilyMemberDocument(clientId: string, familyMemberId: string, file: File, docName: string) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('name', docName);
|
|
formData.append('description', docName);
|
|
this.clientsService.uploadFamilyMemberDocument(clientId, familyMemberId, formData).subscribe({ error: () => {} });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save & Next Confirmation Dialog
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-save-confirm-dialog',
|
|
template: `
|
|
<div class="scd">
|
|
<div class="scd-header">
|
|
<div class="scd-header__icon-wrap">
|
|
<mat-icon class="scd-header__icon">save</mat-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="scd-header__title">Save & Proceed</h3>
|
|
<p class="scd-header__sub">Client general details will be saved</p>
|
|
</div>
|
|
</div>
|
|
<div class="scd-body">
|
|
<p>Are you sure you want to save the client details and proceed to the next step?</p>
|
|
<div class="scd-info">
|
|
<mat-icon class="scd-info__icon">info</mat-icon>
|
|
<span>Once saved, a client record will be created. You can update the details later.</span>
|
|
</div>
|
|
</div>
|
|
<div class="scd-actions">
|
|
<button mat-stroked-button [mat-dialog-close]="false" class="scd-cancel">
|
|
<mat-icon>close</mat-icon>
|
|
Cancel
|
|
</button>
|
|
<button mat-flat-button color="primary" [mat-dialog-close]="true" class="scd-confirm">
|
|
<mat-icon>check</mat-icon>
|
|
Save & Proceed
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
.scd {
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.scd-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 20px 24px 16px;
|
|
}
|
|
.scd-header__icon-wrap {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 12px;
|
|
background: #eff6ff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.scd-header__icon {
|
|
color: #1074b9;
|
|
font-size: 24px;
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
.scd-header__title {
|
|
margin: 0;
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
.scd-header__sub {
|
|
margin: 2px 0 0;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
.scd-body {
|
|
padding: 0 24px 20px;
|
|
}
|
|
.scd-body p {
|
|
margin: 0 0 14px;
|
|
font-size: 14px;
|
|
color: #374151;
|
|
line-height: 1.5;
|
|
}
|
|
.scd-info {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
background: #f0f9ff;
|
|
border: 1px solid #bae6fd;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
color: #0c4a6e;
|
|
}
|
|
.scd-info__icon {
|
|
font-size: 16px !important;
|
|
width: 16px !important;
|
|
height: 16px !important;
|
|
color: #0284c7;
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
.scd-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
padding: 14px 24px;
|
|
border-top: 1px solid #f1f5f9;
|
|
background: #f8fafc;
|
|
}
|
|
.scd-cancel,
|
|
.scd-confirm {
|
|
display: flex !important;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 13px !important;
|
|
height: 38px !important;
|
|
border-radius: 8px !important;
|
|
}
|
|
.scd-confirm {
|
|
font-weight: 600 !important;
|
|
}
|
|
`
|
|
],
|
|
imports: [
|
|
MatIcon,
|
|
MatDialogClose
|
|
]
|
|
})
|
|
export class SaveConfirmDialogComponent {}
|
|
|
|
/**
|
|
* Verify Client Required Alert Dialog
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-verify-client-alert-dialog',
|
|
template: `
|
|
<div class="vcad">
|
|
<div class="vcad-header">
|
|
<div class="vcad-header__icon-wrap">
|
|
<mat-icon class="vcad-header__icon">shield</mat-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="vcad-header__title">Verification Required</h3>
|
|
<p class="vcad-header__sub">Client name must be screened before saving</p>
|
|
</div>
|
|
</div>
|
|
<div class="vcad-body">
|
|
<p>
|
|
Please click <strong>Verify Client</strong> to screen the client name against the UN Security Council
|
|
Consolidated Sanctions List before proceeding.
|
|
</p>
|
|
<div class="vcad-info">
|
|
<mat-icon class="vcad-info__icon">gavel</mat-icon>
|
|
<span
|
|
>RBI KYC Master Direction (2016) — Section 51 & PMLA 2002. Sanctions screening is mandatory before
|
|
account opening.</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="vcad-actions">
|
|
<button mat-flat-button color="primary" [mat-dialog-close]="true" class="vcad-ok">
|
|
<mat-icon>check</mat-icon>
|
|
OK, Verify Now
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
.vcad {
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.vcad-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 20px 24px 16px;
|
|
}
|
|
.vcad-header__icon-wrap {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 12px;
|
|
background: #fef3c7;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.vcad-header__icon {
|
|
color: #d97706;
|
|
font-size: 24px;
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
.vcad-header__title {
|
|
margin: 0;
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
.vcad-header__sub {
|
|
margin: 2px 0 0;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
.vcad-body {
|
|
padding: 0 24px 20px;
|
|
}
|
|
.vcad-body p {
|
|
margin: 0 0 14px;
|
|
font-size: 14px;
|
|
color: #374151;
|
|
line-height: 1.5;
|
|
}
|
|
.vcad-info {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
background: #fff7ed;
|
|
border: 1px solid #fed7aa;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
color: #7c2d12;
|
|
}
|
|
.vcad-info__icon {
|
|
font-size: 16px !important;
|
|
width: 16px !important;
|
|
height: 16px !important;
|
|
color: #d97706;
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
.vcad-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
padding: 14px 24px;
|
|
border-top: 1px solid #f1f5f9;
|
|
background: #f8fafc;
|
|
}
|
|
.vcad-ok {
|
|
display: flex !important;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 13px !important;
|
|
height: 38px !important;
|
|
border-radius: 8px !important;
|
|
font-weight: 600 !important;
|
|
}
|
|
`
|
|
],
|
|
imports: [
|
|
MatIcon,
|
|
MatDialogClose
|
|
]
|
|
})
|
|
export class VerifyClientAlertDialogComponent {}
|