Frontend counterpart to the backend dual-OTP contact-change feature:
- New ContactOtpBlockComponent (6-box segmented OTP input) used up to 4x in
Edit Client (old/new mobile, old/new email — grouped under per-field
sub-headers) and once each for mobile/email in Create Client, gating
Save/Save & Next until every required box is verified.
- edit-client.component.ts: detects mobile/email/DOB changes against the
loaded snapshot using local-date comparison (not `.toISOString()`, which
false-triggers a day-shift in positive-UTC-offset timezones) and
trimmed/case-insensitive email comparison (autofill whitespace or re-typed
casing no longer reads as a change the masked preview can't explain).
- client-kyc-step.component.ts: new `invalidFieldLabels` getter names every
currently-invalid required KYC field, surfaced via the Save & Next tooltip
in both create-client and edit-client so a greyed-out button always
explains what's still missing instead of a generic message.
- CaptureImageDialogComponent generalized for both profile photo and
signature (photograph-a-paper-signature) capture:
- Frame aspect now matches the requested getUserMedia resolution
(4:3 profile / 16:9 signature) with object-fit: cover, eliminating the
pillarboxing (black bars) a mismatched box previously caused.
- Suppresses Chromium's auto-injected PiP/cast corner overlay icons.
- Frame height capped in vh (not just px) so the dialog fits short
viewports instead of pushing Cancel/Upload below the fold.
- upload-document-dialog.component.ts: generic Documents-tab upload now also
offers "Capture from Webcam" alongside Browse, reusing the same capture
dialog.
- New client History tab (reuses the existing Audit Trail data, scoped to
this client) and dataUrlToFile/dataUrlToBlob util for routing webcam
captures through the same upload pipelines as a picked file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2360 lines
82 KiB
TypeScript
2360 lines
82 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,
|
|
OnInit,
|
|
OnDestroy,
|
|
OnChanges,
|
|
SimpleChanges,
|
|
Input,
|
|
Output,
|
|
EventEmitter,
|
|
inject
|
|
} from '@angular/core';
|
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|
import {
|
|
UntypedFormBuilder,
|
|
UntypedFormGroup,
|
|
Validators,
|
|
UntypedFormControl,
|
|
ReactiveFormsModule
|
|
} from '@angular/forms';
|
|
import { Subject, Subscription, merge } from 'rxjs';
|
|
import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
|
import { ClientsService } from 'app/clients/clients.service';
|
|
import { Dates } from 'app/core/utils/dates';
|
|
import { LegalFormId } from 'app/clients/models/legal-form.enum';
|
|
import { ExternalNationalIdService } from 'app/clients/services/external-national-id.service';
|
|
|
|
/** Custom Services */
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { MatDivider } from '@angular/material/divider';
|
|
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { MatIcon } from '@angular/material/icon';
|
|
import { MatSelectTrigger } from '@angular/material/select';
|
|
import { MatTooltip } from '@angular/material/tooltip';
|
|
import { MatDialog, MAT_DIALOG_DATA, MatDialogContent, MatDialogClose } from '@angular/material/dialog';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
|
import { SanctionsVerifyDialogComponent } from './sanctions-verify-dialog.component';
|
|
import { DipVerifyDialogComponent } from './dip-verify-dialog.component';
|
|
import { DinVerifyDialogComponent } from './din-verify-dialog.component';
|
|
import {
|
|
ContactOtpBlockComponent,
|
|
ContactOtpState
|
|
} from 'app/clients/edit-client/contact-otp-block/contact-otp-block.component';
|
|
|
|
/**
|
|
* Create Client Component
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-client-general-step',
|
|
templateUrl: './client-general-step.component.html',
|
|
styleUrls: ['./client-general-step.component.scss'],
|
|
providers: [ExternalNationalIdService],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatDivider,
|
|
CdkTextareaAutosize,
|
|
MatCheckbox,
|
|
FaIconComponent,
|
|
MatIcon,
|
|
MatTooltip,
|
|
MatSelectTrigger,
|
|
DateFormatPipe,
|
|
ContactOtpBlockComponent
|
|
]
|
|
})
|
|
export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges {
|
|
private formBuilder = inject(UntypedFormBuilder);
|
|
private dateUtils = inject(Dates);
|
|
private settingsService = inject(SettingsService);
|
|
private clientService = inject(ClientsService);
|
|
externalNationalIdService = inject(ExternalNationalIdService);
|
|
private dialog = inject(MatDialog);
|
|
private sanitizer = inject(DomSanitizer);
|
|
|
|
/** Subject to trigger unsubscription on destroy */
|
|
private destroy$ = new Subject<void>();
|
|
|
|
@Output() legalFormChangeEvent = new EventEmitter<{ legalForm: number }>();
|
|
|
|
/** Expose enum to template */
|
|
readonly LegalFormId = LegalFormId;
|
|
|
|
/** Minimum date allowed. */
|
|
minDate = new Date(2000, 0, 1);
|
|
/** Maximum date allowed. */
|
|
maxDate = new Date();
|
|
|
|
/** Client Template */
|
|
@Input() clientTemplate: any;
|
|
/** Existing client data for edit mode (null for create) */
|
|
@Input() clientData: any = null;
|
|
/** Pre-fill data from "Create as Client" on a manual family member */
|
|
@Input() prefillData: any = null;
|
|
/** Read-only mode — disables form editing until user clicks edit icon */
|
|
@Input() readOnly = false;
|
|
/** Create Client Form */
|
|
createClientForm: UntypedFormGroup;
|
|
|
|
/** Office Options */
|
|
officeOptions: any;
|
|
/** Staff Options */
|
|
staffOptions: any;
|
|
/** Legal Form Options */
|
|
legalFormOptions: any;
|
|
/** Client Type Options */
|
|
clientTypeOptions: any;
|
|
/** Client Classification Options */
|
|
clientClassificationTypeOptions: any;
|
|
/** Business Line Options */
|
|
businessLineOptions: any;
|
|
/** Constitution Options */
|
|
constitutionOptions: any;
|
|
/** Enriched constitution options (API IDs + master metadata) */
|
|
enrichedConstitutionOptions: any[] = [];
|
|
/** Gender Options */
|
|
genderOptions: any;
|
|
/** Saving Product Options */
|
|
savingProductOptions: any;
|
|
/** Occupation Category Options */
|
|
occupationCategoryOptions: any;
|
|
/** Occupation Options */
|
|
occupationOptions: any;
|
|
/** Salutation Options */
|
|
salutationOptions: any;
|
|
/** Marital Status Options */
|
|
maritalStatusOptions: any[] = [];
|
|
/** Education Level Options */
|
|
educationLevelOptions: any[] = [];
|
|
/** Degree / Qualification Options */
|
|
degreeQualificationOptions: any[] = [];
|
|
/** Religion Options */
|
|
religionOptions: any[] = [];
|
|
/** Social Category Options */
|
|
socialCategoryOptions: any[] = [];
|
|
/** Nationality Options */
|
|
nationalityOptions = [
|
|
{ value: 'INDIAN', label: 'Indian', flagType: 'india', icon: '' },
|
|
{ value: 'NRI', label: 'NRI', flagType: 'none', icon: 'flight_takeoff' },
|
|
{ value: 'FOREIGN_NATIONAL', label: 'Foreign National', flagType: 'none', icon: 'public' },
|
|
{ value: 'PIO', label: 'PIO', flagType: 'none', icon: 'connecting_airports' },
|
|
{ value: 'OCI', label: 'OCI', flagType: 'none', icon: 'card_travel' }
|
|
];
|
|
/** Calculated age detail string (Y/M/D) */
|
|
calculatedAgeDetail: string | null = null;
|
|
/** Whether client is a minor (age < 10) */
|
|
isMinor = false;
|
|
/** Age breakdown for DB storage */
|
|
ageBreakdown = { years: 0, months: 0, days: 0 };
|
|
|
|
/** Guardian search */
|
|
guardianSearchText = '';
|
|
guardianSearchResults: any[] = [];
|
|
guardianSearching = false;
|
|
selectedGuardian: any = null;
|
|
private guardianSearchTimer: any = null;
|
|
/** Signatory search (Non-Individual, multiple) */
|
|
signatorySearchText = '';
|
|
signatorySearchResults: any[] = [];
|
|
signatorySearching = false;
|
|
signatories: any[] = [];
|
|
private signatorySearchTimer: any = null;
|
|
/** Guardian linking mode (search visible) */
|
|
guardianLinking = false;
|
|
/** Guardian detail row expanded */
|
|
guardianExpanded = false;
|
|
/** Mobile number DIP verification status */
|
|
mobileVerified = false;
|
|
/** DIP lookup log ID from API */
|
|
dipLookupLogId: number | null = null;
|
|
/** DIP lookup timestamp from API */
|
|
dipTimestamp: string | null = null;
|
|
/** Alternate mobile number DIP verification status */
|
|
alternateMobileVerified = false;
|
|
/** Alternate DIP lookup log ID from API */
|
|
alternateDipLookupLogId: number | null = null;
|
|
/** Alternate DIP lookup timestamp from API */
|
|
alternateDipTimestamp: string | null = null;
|
|
|
|
/**
|
|
* CLIENT CREATION contact-OTP verification state (mobileNo / emailAddress). Unlike the
|
|
* edit-flow dual old/new OTP panel in `EditClientComponent`, a brand-new client has no "old"
|
|
* destination to verify against — so only a single "new value" OTP-verification is needed
|
|
* per field here. Sent to the backend as `mobileNewOtpId`/`emailNewOtpId` alongside the rest
|
|
* of the create-client payload (see `CreateClientComponent.saveAndProceed()`), which requires
|
|
* both whenever `mobileNo`/`emailAddress` are present (and they always are — both are
|
|
* `Validators.required` on this form).
|
|
*/
|
|
mobileOtpVerified = false;
|
|
mobileNewOtpId: number | null = null;
|
|
emailOtpVerified = false;
|
|
emailNewOtpId: number | null = null;
|
|
|
|
/**
|
|
* Whether both mobile and email have been OTP-verified for CLIENT CREATION. Always `true`
|
|
* in edit mode (`clientData` set) — edit-time contact verification is gated separately by
|
|
* `EditClientComponent.contactVerificationComplete`, not by this component.
|
|
*/
|
|
get contactVerified(): boolean {
|
|
if (this.clientData) {
|
|
return true;
|
|
}
|
|
return this.mobileOtpVerified && this.emailOtpVerified;
|
|
}
|
|
|
|
/** Resets create-time OTP verification for mobileNo — called whenever the field's value
|
|
* changes after a prior verification, since that OTP no longer applies to the new value. */
|
|
onMobileOtpState(state: ContactOtpState): void {
|
|
this.mobileOtpVerified = state.verified;
|
|
this.mobileNewOtpId = state.otpId;
|
|
}
|
|
|
|
/** Resets create-time OTP verification for emailAddress — see `onMobileOtpState()`. */
|
|
onEmailOtpState(state: ContactOtpState): void {
|
|
this.emailOtpVerified = state.verified;
|
|
this.emailNewOtpId = state.otpId;
|
|
}
|
|
/** Constitution Types Master with KYC field metadata */
|
|
constitutionTypesMaster = [
|
|
{
|
|
value: 'PRIVATE_LIMITED',
|
|
label: 'Private Limited Company',
|
|
icon: 'business',
|
|
panLabel: 'PAN of Company',
|
|
panHint: 'PAN mandatory. CIN from MCA.',
|
|
regHint: 'Certificate of Incorporation No.',
|
|
showCin: true,
|
|
additionalField: 'Constitution as per Articles of Association',
|
|
documents: 'Certificate of Incorporation, Memorandum & Articles of Association, PAN, Board Resolution',
|
|
signatoryKyc: 'KYC of all Authorized Signatories (OVDs + PAN + Photo)',
|
|
note: 'Board Resolution to specify authorized signatories'
|
|
},
|
|
{
|
|
value: 'PUBLIC_LIMITED',
|
|
label: 'Public Limited Company',
|
|
icon: 'domain',
|
|
panLabel: 'PAN of Company',
|
|
panHint: 'PAN mandatory. CIN from MCA.',
|
|
regHint: 'Certificate of Incorporation No.',
|
|
showCin: true,
|
|
additionalField: 'Constitution as per Articles of Association',
|
|
documents: 'Certificate of Incorporation, Memorandum & Articles of Association, PAN, Board Resolution',
|
|
signatoryKyc: 'KYC of all Authorized Signatories (OVDs + PAN + Photo)',
|
|
note: 'Board Resolution to specify authorized signatories'
|
|
},
|
|
{
|
|
value: 'LLP',
|
|
label: 'Limited Liability Partnership (LLP)',
|
|
icon: 'handshake',
|
|
panLabel: 'PAN of LLP',
|
|
panHint: 'PAN mandatory. LLPIN from MCA.',
|
|
regHint: 'Certificate of Incorporation No.',
|
|
showCin: true,
|
|
additionalField: 'LLP Agreement',
|
|
documents: 'Certificate of Incorporation, LLP Agreement, PAN, Designated Partners Resolution',
|
|
signatoryKyc: 'KYC of all Designated Partners / Authorized Signatories',
|
|
note: 'LLPIN from MCA required'
|
|
},
|
|
{
|
|
value: 'PARTNERSHIP',
|
|
label: 'Partnership Firm',
|
|
icon: 'handshake',
|
|
panLabel: 'PAN of Firm',
|
|
panHint: 'PAN of Firm mandatory',
|
|
regHint: 'Registration Certificate No. (if registered)',
|
|
showCin: false,
|
|
additionalField: 'Partners Names (linked client-wise)',
|
|
documents: 'Partnership Deed, Registration Certificate (if registered), PAN of Firm',
|
|
signatoryKyc: 'KYC of all Partners / Authorized Signatories',
|
|
note: 'Registration optional but preferred'
|
|
},
|
|
{
|
|
value: 'PROPRIETORSHIP',
|
|
label: 'Proprietorship',
|
|
icon: 'person',
|
|
panLabel: 'PAN of Proprietor',
|
|
panHint: 'PAN of Proprietor (no separate entity PAN needed)',
|
|
regHint: 'GST / Shop License No.',
|
|
showCin: false,
|
|
additionalField: '',
|
|
documents: 'GST Certificate / Shop License / Utility Bill, PAN of Proprietor',
|
|
signatoryKyc: "Proprietor's individual KYC (same as individual requirements)",
|
|
note: 'At least 2 proofs of business existence required. Proprietor name = Client name.'
|
|
},
|
|
{
|
|
value: 'HUF',
|
|
label: 'HUF (Hindu Undivided Family)',
|
|
icon: 'family_restroom',
|
|
panLabel: 'PAN of HUF',
|
|
panHint: 'PAN of HUF mandatory',
|
|
regHint: 'HUF Declaration / Deed No.',
|
|
showCin: false,
|
|
additionalField: 'Karta Name',
|
|
documents: 'HUF Declaration/Deed, PAN of HUF',
|
|
signatoryKyc: 'KYC of Karta',
|
|
note: ''
|
|
},
|
|
{
|
|
value: 'TRUST',
|
|
label: 'Trust',
|
|
icon: 'verified_user',
|
|
panLabel: 'PAN of Trust',
|
|
panHint: 'PAN of Trust mandatory',
|
|
regHint: 'Trust Deed / Registration No.',
|
|
showCin: false,
|
|
additionalField: 'Purpose of Trust',
|
|
documents: 'Trust Deed, Registration Certificate, PAN of Trust',
|
|
signatoryKyc: 'KYC of Trustees / Authorized Signatories',
|
|
note: 'Foreign contribution rules apply if applicable'
|
|
},
|
|
{
|
|
value: 'SOCIETY',
|
|
label: 'Society',
|
|
icon: 'groups',
|
|
panLabel: 'PAN of Society',
|
|
panHint: 'PAN of Society mandatory',
|
|
regHint: 'Registration Certificate No.',
|
|
showCin: false,
|
|
additionalField: 'Managing Committee Details',
|
|
documents: 'Registration Certificate, PAN of Society, Resolution authorizing signatories',
|
|
signatoryKyc: 'KYC of Authorized Signatories',
|
|
note: 'Governed by Societies Registration Act'
|
|
},
|
|
{
|
|
value: 'ASSOCIATION',
|
|
label: 'Association / Club',
|
|
icon: 'diversity_3',
|
|
panLabel: 'PAN',
|
|
panHint: 'PAN mandatory',
|
|
regHint: 'Registration Certificate No.',
|
|
showCin: false,
|
|
additionalField: 'Club Constitution Document',
|
|
documents: 'Registration Certificate, PAN, Resolution authorizing signatories',
|
|
signatoryKyc: 'KYC of Authorized Signatories',
|
|
note: 'Committee approval needed for account'
|
|
},
|
|
{
|
|
value: 'GOVT_PSU',
|
|
label: 'Government Dept / PSU',
|
|
icon: 'account_balance',
|
|
panLabel: 'PAN (if applicable)',
|
|
panHint: 'PAN if applicable',
|
|
regHint: 'Govt. Notification / Order No.',
|
|
showCin: false,
|
|
additionalField: 'Gazette Notification Reference',
|
|
documents: 'Relevant Government Notification / Order, PAN (if applicable)',
|
|
signatoryKyc: 'Authorized Signatory KYC',
|
|
note: 'Special KYC norms as per RBI circular'
|
|
},
|
|
{
|
|
value: 'COOPERATIVE',
|
|
label: 'Cooperative Society / Cooperative Bank',
|
|
icon: 'hub',
|
|
panLabel: 'PAN',
|
|
panHint: 'PAN mandatory. RBI license if bank.',
|
|
regHint: 'Registration Certificate No.',
|
|
showCin: false,
|
|
additionalField: 'Bye-laws of the Cooperative',
|
|
documents: 'Registration Certificate, PAN, Resolution authorizing signatories',
|
|
signatoryKyc: 'KYC of Authorized Signatories',
|
|
note: 'Separate RBI licensing check for cooperative banks'
|
|
}
|
|
];
|
|
|
|
/** Currently selected constitution metadata */
|
|
get selectedConstitution(): any {
|
|
const val = this.createClientForm.get('clientNonPersonDetails.constitutionId')?.value;
|
|
return this.enrichedConstitutionOptions.find((c: any) => c.id === val) || null;
|
|
}
|
|
|
|
/** Sanctions verification state */
|
|
sanctionsVerified = false;
|
|
/** Sanctions match was found — blocks account opening */
|
|
sanctionsBlocked = false;
|
|
/** Screening log ID from sanctions API */
|
|
screeningLogId: number | null = null;
|
|
/** Snapshot of the name that was last verified — used to invalidate verification when name changes */
|
|
private verifiedName: string = '';
|
|
|
|
/**
|
|
* @param {FormBuilder} formBuilder Form Builder
|
|
* @param {Dates} dateUtils Date Utils
|
|
* @param {SettingsService} settingsService Setting service
|
|
* @param {ClientsService} clientService Client service
|
|
*/
|
|
constructor() {
|
|
this.setClientForm();
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.maxDate = this.settingsService.businessDate;
|
|
this.setOptions();
|
|
// Fetch marital status options from code values
|
|
this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => {
|
|
this.maritalStatusOptions = options || [];
|
|
});
|
|
// Fetch education level options from code values
|
|
this.clientService.getCodeValuesByName('EducationLevel').subscribe((options) => {
|
|
this.educationLevelOptions = options || [];
|
|
});
|
|
// Fetch degree / qualification options from code values
|
|
this.clientService.getCodeValuesByName('DegreeQualification').subscribe((options) => {
|
|
this.degreeQualificationOptions = options || [];
|
|
});
|
|
// Fetch religion options from code values
|
|
this.clientService.getCodeValuesByName('Religion').subscribe((options) => {
|
|
this.religionOptions = options || [];
|
|
});
|
|
// Fetch social category options from code values
|
|
this.clientService.getCodeValuesByName('SocialCategory').subscribe((options) => {
|
|
this.socialCategoryOptions = options || [];
|
|
});
|
|
// Reset degree selection whenever education level changes (degree list is filtered by level)
|
|
this.createClientForm
|
|
.get('educationLevelId')
|
|
?.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
const current = this.createClientForm.get('degreeQualificationId')?.value;
|
|
if (current && !this.filteredDegreeQualificationOptions.some((d: any) => d.id === current)) {
|
|
this.createClientForm.get('degreeQualificationId')?.setValue('');
|
|
}
|
|
});
|
|
// Fetch all constitution options from code ID 17
|
|
this.clientService.getCodeValuesById(17).subscribe((options) => {
|
|
if (options?.length) {
|
|
this.constitutionOptions = options;
|
|
// Sort master by value length descending so longer/more-specific keys match first
|
|
const sortedMaster = [...this.constitutionTypesMaster].sort((a, b) => b.value.length - a.value.length);
|
|
this.enrichedConstitutionOptions = options.map((opt: any) => {
|
|
const normalized = opt.name?.toUpperCase().replace(/[\s_-]+/g, '') || '';
|
|
const master = sortedMaster.find((m) => normalized.includes(m.value.replace(/_/g, '')));
|
|
return {
|
|
id: opt.id,
|
|
value: master?.value || opt.name,
|
|
label: master?.label || opt.name,
|
|
icon: master?.icon || 'business',
|
|
panLabel: master?.panLabel || '',
|
|
panHint: master?.panHint || '',
|
|
regHint: master?.regHint || '',
|
|
showCin: master?.showCin || false,
|
|
additionalField: master?.additionalField || '',
|
|
documents: master?.documents || '',
|
|
signatoryKyc: master?.signatoryKyc || '',
|
|
note: master?.note || ''
|
|
};
|
|
});
|
|
}
|
|
});
|
|
this.buildDependencies();
|
|
if (this.clientData) {
|
|
this.prefillForEdit();
|
|
} else if (this.prefillData) {
|
|
this.prefillFromFamilyMember();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pre-fills the General step form from a manually-entered family member
|
|
* when the user clicks "Create as Client" on the family members tab.
|
|
*/
|
|
private prefillFromFamilyMember(): void {
|
|
const d = this.prefillData;
|
|
if (!d) return;
|
|
|
|
// Parse DOB — Fineract returns [yyyy, mm, dd] array or ISO string
|
|
let dob: Date | undefined;
|
|
if (d.dateOfBirth) {
|
|
if (Array.isArray(d.dateOfBirth)) {
|
|
dob = new Date(d.dateOfBirth[0], d.dateOfBirth[1] - 1, d.dateOfBirth[2]);
|
|
} else {
|
|
const parsed = new Date(d.dateOfBirth);
|
|
if (!isNaN(parsed.getTime())) dob = parsed;
|
|
}
|
|
}
|
|
|
|
// Patch base fields first
|
|
this.createClientForm.patchValue({
|
|
mobileNo: d.mobileNo || '',
|
|
...(dob ? { dateOfBirth: dob } : {})
|
|
});
|
|
|
|
// Name controls are added dynamically by buildDependencies via legalFormId — set after tick
|
|
setTimeout(() => {
|
|
if (this.createClientForm.get('firstname') && d.firstname) {
|
|
this.createClientForm.get('firstname').setValue(d.firstname);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pre-fills the form with existing client data for edit mode.
|
|
* Called AFTER buildDependencies so name controls already exist.
|
|
*/
|
|
private prefillForEdit() {
|
|
const data = this.clientData;
|
|
|
|
// Patch all base controls that exist on the form
|
|
this.createClientForm.patchValue({
|
|
officeId: data.officeId,
|
|
isStaff: data.isStaff,
|
|
active: data.active,
|
|
accountNo: data.accountNo,
|
|
genderId: data.gender?.id,
|
|
maritalStatusId: data.maritalStatus?.id || '',
|
|
educationLevelId: data.educationLevel?.id || data.educationLevelId || '',
|
|
degreeQualificationId: data.degreeQualification?.id || data.degreeQualificationId || '',
|
|
religionId: data.religion?.id || data.religionId || '',
|
|
socialCategoryId: data.socialCategory?.id || data.socialCategoryId || '',
|
|
mobileNo: data.mobileNo,
|
|
alternateMobileNo: data.alternateMobileNo || '',
|
|
emailAddress: data.emailAddress,
|
|
dateOfBirth: data.dateOfBirth ? new Date(data.dateOfBirth) : '',
|
|
occupationCategoryId: data.occupationCategory?.id || data.occupationCategoryId || '',
|
|
occupationId: data.occupation?.id || data.occupationId || '',
|
|
monthlyIncome: data.monthlyIncome || '',
|
|
annualTurnover: data.annualTurnover || '',
|
|
agricultureIncome: data.agricultureIncome || '',
|
|
nonAgricultureIncome: data.nonAgricultureIncome || '',
|
|
submittedOnDate: data.timeline?.submittedOnDate
|
|
? new Date(data.timeline.submittedOnDate)
|
|
: this.settingsService.businessDate
|
|
});
|
|
|
|
// Patch name fields (already created by buildDependencies via legalFormId subscription)
|
|
const legalFormId = data.legalForm?.id || LegalFormId.PERSON;
|
|
if (legalFormId === LegalFormId.PERSON) {
|
|
if (this.createClientForm.get('salutationId')) {
|
|
this.createClientForm.get('salutationId').setValue(data.salutation?.id || data.salutationId || '');
|
|
}
|
|
if (this.createClientForm.get('firstname')) {
|
|
this.createClientForm.get('firstname').setValue(data.firstname || '');
|
|
}
|
|
if (this.createClientForm.get('middlename')) {
|
|
this.createClientForm.get('middlename').setValue(data.middlename || '');
|
|
}
|
|
if (this.createClientForm.get('lastname')) {
|
|
this.createClientForm.get('lastname').setValue(data.lastname || '');
|
|
}
|
|
} else if (this.createClientForm.get('fullname')) {
|
|
this.createClientForm.get('fullname').setValue(data.fullname || '');
|
|
if (data.clientNonPersonDetails && this.createClientForm.get('clientNonPersonDetails')) {
|
|
this.createClientForm.get('clientNonPersonDetails').patchValue({
|
|
constitutionId: data.clientNonPersonDetails.constitution?.id || '',
|
|
mainBusinessLineId: data.clientNonPersonDetails.mainBusinessLine?.id || '',
|
|
incorpNumber: data.clientNonPersonDetails.incorpNumber || '',
|
|
remarks: data.clientNonPersonDetails.remarks || '',
|
|
authorizedSignatory: data.clientNonPersonDetails.authorizedSignatory || '',
|
|
additionalInfo: data.clientNonPersonDetails.additionalInfo || ''
|
|
});
|
|
}
|
|
}
|
|
|
|
// Restore DIP / sanctions verification state from existing client data
|
|
if (data.dipVerified === true || data.dipVerified === 'true' || data.dipVerified === 1) {
|
|
this.mobileVerified = true;
|
|
}
|
|
if (data.dipLookupLogId) {
|
|
this.dipLookupLogId = data.dipLookupLogId;
|
|
}
|
|
if (data.dipTimestamp) {
|
|
this.dipTimestamp = data.dipTimestamp;
|
|
}
|
|
if (data.alternateDipVerified === true || data.alternateDipVerified === 'true' || data.alternateDipVerified === 1) {
|
|
this.alternateMobileVerified = true;
|
|
}
|
|
if (data.alternateDipLookupLogId) {
|
|
this.alternateDipLookupLogId = data.alternateDipLookupLogId;
|
|
}
|
|
if (data.alternateDipTimestamp) {
|
|
this.alternateDipTimestamp = data.alternateDipTimestamp;
|
|
}
|
|
if (data.nameVerified === true || data.nameVerified === 'true' || data.nameVerified === 1) {
|
|
this.sanctionsVerified = true;
|
|
this.verifiedName = this.currentNameSnapshot();
|
|
} else if (data.nameVerified === false || data.nameVerified === 'false' || data.nameVerified === 0) {
|
|
this.sanctionsVerified = false;
|
|
}
|
|
if (data.screeningLogId) {
|
|
this.screeningLogId = data.screeningLogId;
|
|
}
|
|
|
|
// Set activation date if client is active (activationDate control is added by active subscription)
|
|
if (data.active && data.timeline?.activatedOnDate) {
|
|
setTimeout(() => {
|
|
if (this.createClientForm.get('activationDate')) {
|
|
this.createClientForm.get('activationDate').setValue(new Date(data.timeline.activatedOnDate));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the client form.
|
|
*/
|
|
setClientForm() {
|
|
this.createClientForm = this.formBuilder.group({
|
|
officeId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
legalFormId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
nationalityId: ['INDIAN'],
|
|
isStaff: [false],
|
|
active: [false],
|
|
addSavings: [false],
|
|
accountNo: [''],
|
|
genderId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
maritalStatusId: [''],
|
|
educationLevelId: [''],
|
|
degreeQualificationId: [''],
|
|
religionId: [''],
|
|
socialCategoryId: [''],
|
|
mobileNo: [
|
|
'',
|
|
[
|
|
Validators.required,
|
|
Validators.pattern(/^[6-9]\d{9}$/),
|
|
Validators.maxLength(10)
|
|
]
|
|
],
|
|
alternateMobileNo: [
|
|
'',
|
|
[
|
|
Validators.pattern(/^[6-9]\d{9}$/),
|
|
Validators.maxLength(10)
|
|
]
|
|
],
|
|
emailAddress: [
|
|
'',
|
|
[
|
|
Validators.required,
|
|
Validators.email,
|
|
Validators.pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/),
|
|
Validators.maxLength(100)
|
|
]
|
|
],
|
|
dateOfBirth: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
occupationCategoryId: [''],
|
|
occupationId: [''],
|
|
monthlyIncome: [
|
|
'',
|
|
[
|
|
Validators.pattern(/^\d+(\.\d{1,2})?$/),
|
|
Validators.min(0)
|
|
]
|
|
],
|
|
annualTurnover: [
|
|
'',
|
|
[
|
|
Validators.pattern(/^\d+(\.\d{1,2})?$/),
|
|
Validators.min(0)
|
|
]
|
|
],
|
|
agricultureIncome: [
|
|
'',
|
|
[
|
|
Validators.pattern(/^\d+(\.\d{1,2})?$/),
|
|
Validators.min(0)
|
|
]
|
|
],
|
|
nonAgricultureIncome: [
|
|
'',
|
|
[
|
|
Validators.pattern(/^\d+(\.\d{1,2})?$/),
|
|
Validators.min(0)
|
|
]
|
|
],
|
|
submittedOnDate: [
|
|
this.settingsService.businessDate,
|
|
Validators.required
|
|
]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets select dropdown options.
|
|
*/
|
|
setOptions() {
|
|
this.officeOptions = this.clientTemplate.officeOptions;
|
|
this.staffOptions = this.clientTemplate.staffOptions;
|
|
this.legalFormOptions = this.clientTemplate.clientLegalFormOptions;
|
|
this.clientTypeOptions = this.clientTemplate.clientTypeOptions;
|
|
this.clientClassificationTypeOptions = this.clientTemplate.clientClassificationOptions;
|
|
this.businessLineOptions = this.clientTemplate.clientNonPersonMainBusinessLineOptions;
|
|
this.constitutionOptions = this.clientTemplate.clientNonPersonConstitutionOptions;
|
|
this.genderOptions = this.clientTemplate.genderOptions;
|
|
this.savingProductOptions = this.clientTemplate.savingProductOptions;
|
|
this.occupationCategoryOptions = this.clientTemplate.occupationCategoryOptions || [];
|
|
this.occupationOptions = this.clientTemplate.occupationOptions || [];
|
|
this.salutationOptions = this.clientTemplate.salutationOptions || [];
|
|
}
|
|
|
|
/**
|
|
* Adds controls conditionally.
|
|
*/
|
|
buildDependencies() {
|
|
this.createClientForm
|
|
.get('legalFormId')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe((legalFormId: number) => {
|
|
this.legalFormChangeEvent.emit({ legalForm: legalFormId });
|
|
if (legalFormId === LegalFormId.PERSON) {
|
|
this.createClientForm.removeControl('fullname');
|
|
this.createClientForm.removeControl('clientNonPersonDetails');
|
|
// Restore DOB and Gender required for individual
|
|
this.createClientForm.get('dateOfBirth')?.setValidators(Validators.required);
|
|
this.createClientForm.get('dateOfBirth')?.updateValueAndValidity();
|
|
this.createClientForm.get('genderId')?.setValidators(Validators.required);
|
|
this.createClientForm.get('genderId')?.updateValueAndValidity();
|
|
this.createClientForm.addControl('salutationId', new UntypedFormControl(''));
|
|
this.createClientForm.addControl(
|
|
'firstname',
|
|
new UntypedFormControl('', [
|
|
Validators.required,
|
|
Validators.pattern(/^[A-Za-z][A-Za-z\s.'-]*$/),
|
|
Validators.maxLength(50)
|
|
])
|
|
);
|
|
this.createClientForm.addControl(
|
|
'middlename',
|
|
new UntypedFormControl('', [
|
|
Validators.pattern(/^[A-Za-z][A-Za-z\s.'-]*$/),
|
|
Validators.maxLength(50)
|
|
])
|
|
);
|
|
this.createClientForm.addControl(
|
|
'lastname',
|
|
new UntypedFormControl('', [
|
|
Validators.required,
|
|
Validators.pattern(/^[A-Za-z][A-Za-z\s.'-]*$/),
|
|
Validators.maxLength(50)
|
|
])
|
|
);
|
|
} else {
|
|
this.createClientForm.removeControl('salutationId');
|
|
this.createClientForm.removeControl('firstname');
|
|
this.createClientForm.removeControl('middlename');
|
|
this.createClientForm.removeControl('lastname');
|
|
// Remove DOB and Gender required for non-individual
|
|
this.createClientForm.get('dateOfBirth')?.clearValidators();
|
|
this.createClientForm.get('dateOfBirth')?.updateValueAndValidity();
|
|
this.createClientForm.get('genderId')?.clearValidators();
|
|
this.createClientForm.get('genderId')?.updateValueAndValidity();
|
|
this.createClientForm.addControl(
|
|
'fullname',
|
|
new UntypedFormControl('', [
|
|
Validators.required,
|
|
Validators.pattern(/^[A-Za-z][A-Za-z\s.&'-]*$/),
|
|
Validators.maxLength(100)
|
|
])
|
|
);
|
|
this.createClientForm.addControl(
|
|
'clientNonPersonDetails',
|
|
this.formBuilder.group({
|
|
constitutionId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
mainBusinessLineId: [''],
|
|
incorpNumber: [''],
|
|
authorizedSignatory: [''],
|
|
additionalInfo: [''],
|
|
remarks: ['']
|
|
})
|
|
);
|
|
}
|
|
// Re-attach name-change watchers after controls are (re)created
|
|
setTimeout(() => this.watchNameChangesForReverification());
|
|
});
|
|
// Initialize legal form — use client's existing value in edit mode, default to PERSON for create
|
|
const initialLegalForm = this.clientData?.legalForm?.id || LegalFormId.PERSON;
|
|
this.createClientForm.get('legalFormId').patchValue(initialLegalForm);
|
|
// Salutation → Gender auto-fill
|
|
this.createClientForm
|
|
.get('legalFormId')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
// Re-subscribe after salutationId is added
|
|
setTimeout(() => this.watchSalutationForGender());
|
|
});
|
|
setTimeout(() => this.watchSalutationForGender());
|
|
// Age auto-calculation from DOB (years, months, days) + minor detection
|
|
this.createClientForm
|
|
.get('dateOfBirth')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe((dob: any) => {
|
|
if (dob) {
|
|
const birth = new Date(dob);
|
|
const today = new Date();
|
|
let years = today.getFullYear() - birth.getFullYear();
|
|
let months = today.getMonth() - birth.getMonth();
|
|
let days = today.getDate() - birth.getDate();
|
|
if (days < 0) {
|
|
months--;
|
|
const prevMonth = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
days += prevMonth.getDate();
|
|
}
|
|
if (months < 0) {
|
|
years--;
|
|
months += 12;
|
|
}
|
|
if (years >= 0) {
|
|
this.calculatedAgeDetail = `${years} yr${years !== 1 ? 's' : ''} ${months} mo${months !== 1 ? 's' : ''} ${days} day${days !== 1 ? 's' : ''}`;
|
|
this.ageBreakdown = { years, months, days };
|
|
const wasMinor = this.isMinor;
|
|
this.isMinor = years < 10;
|
|
// Add/remove guardian form group based on minor status
|
|
if (this.isMinor && !wasMinor) {
|
|
this.createClientForm.addControl(
|
|
'guardianDetails',
|
|
this.formBuilder.group({
|
|
guardianClientId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
guardianRelation: [
|
|
'',
|
|
Validators.required
|
|
]
|
|
})
|
|
);
|
|
this.selectedGuardian = null;
|
|
this.guardianSearchText = '';
|
|
this.guardianSearchResults = [];
|
|
} else if (!this.isMinor && wasMinor) {
|
|
this.createClientForm.removeControl('guardianDetails');
|
|
this.selectedGuardian = null;
|
|
}
|
|
} else {
|
|
this.calculatedAgeDetail = null;
|
|
this.isMinor = false;
|
|
this.ageBreakdown = { years: 0, months: 0, days: 0 };
|
|
this.createClientForm.removeControl('guardianDetails');
|
|
}
|
|
} else {
|
|
this.calculatedAgeDetail = null;
|
|
this.isMinor = false;
|
|
this.ageBreakdown = { years: 0, months: 0, days: 0 };
|
|
this.createClientForm.removeControl('guardianDetails');
|
|
}
|
|
});
|
|
this.createClientForm
|
|
.get('active')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe((active: boolean) => {
|
|
if (active) {
|
|
this.createClientForm.addControl('activationDate', new UntypedFormControl('', Validators.required));
|
|
} else {
|
|
this.createClientForm.removeControl('activationDate');
|
|
}
|
|
});
|
|
this.createClientForm
|
|
.get('addSavings')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe((active: boolean) => {
|
|
if (active) {
|
|
this.createClientForm.addControl('savingsProductId', new UntypedFormControl('', Validators.required));
|
|
} else {
|
|
this.createClientForm.removeControl('savingsProductId');
|
|
}
|
|
});
|
|
this.createClientForm
|
|
.get('officeId')
|
|
.valueChanges.pipe(
|
|
filter((officeId: number) => !!officeId),
|
|
switchMap((officeId: number) => this.clientService.getClientWithOfficeTemplate(officeId)),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe((clientTemplate: any) => {
|
|
this.staffOptions = clientTemplate.staffOptions;
|
|
});
|
|
|
|
// Reset DIP verification when mobile number changes
|
|
this.createClientForm
|
|
.get('mobileNo')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
this.mobileVerified = false;
|
|
// Also reset create-time contact-OTP verification — an OTP verified against a
|
|
// previous mobile number no longer applies once the value is edited. Belt-and-braces
|
|
// alongside ContactOtpBlockComponent's own destination-change reset: this fires even
|
|
// if the OTP block was unmounted (e.g. `*ngIf` hid it while the field was momentarily
|
|
// invalid) and its own reset never had a chance to run.
|
|
this.mobileOtpVerified = false;
|
|
this.mobileNewOtpId = null;
|
|
});
|
|
|
|
this.createClientForm
|
|
.get('alternateMobileNo')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
this.alternateMobileVerified = false;
|
|
});
|
|
|
|
// Reset create-time contact-OTP verification when email address changes — see the
|
|
// mobileNo subscription above for the rationale.
|
|
this.createClientForm
|
|
.get('emailAddress')
|
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
this.emailOtpVerified = false;
|
|
this.emailNewOtpId = null;
|
|
});
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
if (changes['readOnly'] && this.createClientForm) {
|
|
if (this.readOnly) {
|
|
this.createClientForm.disable({ emitEvent: false });
|
|
} else {
|
|
this.createClientForm.enable({ emitEvent: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
/**
|
|
* Verify mobile number with DIP via dialog.
|
|
*/
|
|
verifyWithDIP() {
|
|
const mobileNo = this.createClientForm.get('mobileNo')?.value || '';
|
|
if (mobileNo.length !== 10) return;
|
|
const dialogRef = this.dialog.open(DipVerifyDialogComponent, {
|
|
width: '500px',
|
|
data: { mobileNo }
|
|
});
|
|
dialogRef.afterClosed().subscribe((result: any) => {
|
|
if (!result) return;
|
|
if (result.verified) {
|
|
this.mobileVerified = true;
|
|
}
|
|
this.dipLookupLogId = result.lookupLogId || null;
|
|
this.dipTimestamp = result.timestamp || null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify alternate mobile number with DIP via dialog.
|
|
*/
|
|
verifyAlternateDIP() {
|
|
const mobileNo = this.createClientForm.get('alternateMobileNo')?.value || '';
|
|
if (mobileNo.length !== 10) return;
|
|
const dialogRef = this.dialog.open(DipVerifyDialogComponent, {
|
|
width: '500px',
|
|
data: { mobileNo }
|
|
});
|
|
dialogRef.afterClosed().subscribe((result: any) => {
|
|
if (!result) return;
|
|
if (result.verified) {
|
|
this.alternateMobileVerified = true;
|
|
}
|
|
this.alternateDipLookupLogId = result.lookupLogId || null;
|
|
this.alternateDipTimestamp = result.timestamp || null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Zoom an image (guardian photo / signature) in a dialog.
|
|
*/
|
|
zoomGuardianImage(imageUrl: string, title: string) {
|
|
this.dialog.open(GuardianImageZoomDialogComponent, {
|
|
data: { imageUrl, title },
|
|
panelClass: 'guardian-zoom-panel'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Edit guardian link — re-enters linking mode while keeping the current guardian.
|
|
*/
|
|
editGuardianLink() {
|
|
this.guardianLinking = true;
|
|
this.guardianExpanded = false;
|
|
this.removeGuardian();
|
|
}
|
|
|
|
/**
|
|
* Verify client name against sanctions lists via dialog.
|
|
* Passes the client name to the dialog for real API screening.
|
|
* If a match is found, sanctionsBlocked is set to true blocking further steps.
|
|
*/
|
|
verifySanctions() {
|
|
const name = this.currentNameSnapshot();
|
|
|
|
const dialogRef = this.dialog.open(SanctionsVerifyDialogComponent, {
|
|
width: '550px',
|
|
data: { name }
|
|
});
|
|
dialogRef.afterClosed().subscribe((result: any) => {
|
|
if (!result) return;
|
|
this.screeningLogId = result.screeningLogId || null;
|
|
if (result.verified === true) {
|
|
// CLEAR — no match found
|
|
this.sanctionsVerified = true;
|
|
this.sanctionsBlocked = false;
|
|
this.verifiedName = name;
|
|
} else if (result.verified === false) {
|
|
// FLAGGED — match found, block account opening
|
|
this.sanctionsVerified = false;
|
|
this.sanctionsBlocked = true;
|
|
this.verifiedName = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Concatenated current name from the form (handles individual + entity) */
|
|
private currentNameSnapshot(): string {
|
|
const firstname = this.createClientForm.get('firstname')?.value || '';
|
|
const middlename = this.createClientForm.get('middlename')?.value || '';
|
|
const lastname = this.createClientForm.get('lastname')?.value || '';
|
|
const fullname = this.createClientForm.get('fullname')?.value || '';
|
|
return (
|
|
fullname ||
|
|
[
|
|
firstname,
|
|
middlename,
|
|
lastname
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
).trim();
|
|
}
|
|
|
|
/**
|
|
* Watch name fields — once a name has been verified, any edit invalidates the
|
|
* sanctions verification badge so the user must re-verify before submitting.
|
|
*/
|
|
private nameWatchSub: Subscription | null = null;
|
|
private watchNameChangesForReverification() {
|
|
this.nameWatchSub?.unsubscribe();
|
|
const fields = [
|
|
'firstname',
|
|
'middlename',
|
|
'lastname',
|
|
'fullname'
|
|
];
|
|
const controls = fields.map((f) => this.createClientForm.get(f)).filter((c) => !!c);
|
|
if (!controls.length) return;
|
|
this.nameWatchSub = merge(...controls.map((c) => c!.valueChanges))
|
|
.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
// Only invalidate if a verification was previously recorded
|
|
if (!this.sanctionsVerified && !this.sanctionsBlocked) return;
|
|
if (this.currentNameSnapshot() !== this.verifiedName) {
|
|
this.sanctionsVerified = false;
|
|
this.sanctionsBlocked = false;
|
|
this.screeningLogId = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Search existing clients to select as guardian */
|
|
onGuardianSearch(text: string): void {
|
|
this.guardianSearchText = text;
|
|
clearTimeout(this.guardianSearchTimer);
|
|
|
|
if (!text || text.length < 2) {
|
|
this.guardianSearchResults = [];
|
|
return;
|
|
}
|
|
|
|
this.guardianSearching = true;
|
|
this.guardianSearchTimer = setTimeout(() => {
|
|
// Guardian picker: only natural persons can be a guardian.
|
|
this.clientService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
|
|
next: (data: any) => {
|
|
this.guardianSearchResults = data?.content || [];
|
|
this.guardianSearching = false;
|
|
this.clientService.enrichSearchResults(this.guardianSearchResults);
|
|
},
|
|
error: () => {
|
|
this.guardianSearchResults = [];
|
|
this.guardianSearching = false;
|
|
}
|
|
});
|
|
}, 400);
|
|
}
|
|
|
|
/** Select a client as guardian and fetch full details */
|
|
selectGuardian(client: any): void {
|
|
this.selectedGuardian = { ...client, _loading: true };
|
|
this.guardianSearchResults = [];
|
|
this.guardianSearchText = '';
|
|
this.guardianLinking = false;
|
|
this.createClientForm.get('guardianDetails')?.patchValue({
|
|
guardianClientId: client.id
|
|
});
|
|
this.fetchClientFullDetails(client.id, this.selectedGuardian);
|
|
}
|
|
|
|
/** Fetch full client details directly onto target object */
|
|
private fetchClientFullDetails(clientId: number, target: any): void {
|
|
const cid = clientId.toString();
|
|
|
|
this.clientService.getClientData(cid).subscribe({
|
|
next: (data: any) => {
|
|
target.cifNumber = data.cifNumber || data.accountNo;
|
|
target.mobileNo = data.mobileNo || null;
|
|
target.emailAddress = data.emailAddress || null;
|
|
target.officeName = data.officeName || null;
|
|
target._loading = false;
|
|
},
|
|
error: () => {
|
|
target._loading = false;
|
|
}
|
|
});
|
|
|
|
this.clientService.getClientKyc(cid).subscribe({
|
|
next: (kyc: any) => {
|
|
target.panNumber = kyc?.panNumber || '-';
|
|
target.panVerified = kyc?.panVerified || false;
|
|
target.aadhaarMasked = kyc?.aadhaarNumberMasked || '-';
|
|
target.aadhaarVerified = kyc?.aadhaarVerified || false;
|
|
},
|
|
error: () => {
|
|
target.panNumber = '-';
|
|
target.aadhaarMasked = '-';
|
|
}
|
|
});
|
|
|
|
this.clientService.getClientAccountData(cid).subscribe({
|
|
next: (accts: any) => {
|
|
const savings = accts?.savingsAccounts || [];
|
|
const active = savings.find((a: any) => a.status?.active) || savings[0];
|
|
target.accountType = active ? active.productName || 'Savings' : '-';
|
|
target.accountNo = active?.accountNo || '-';
|
|
},
|
|
error: () => {
|
|
target.accountType = '-';
|
|
target.accountNo = '-';
|
|
}
|
|
});
|
|
|
|
this.clientService.getClientAddressData(cid).subscribe({
|
|
next: (addresses: any[]) => {
|
|
if (addresses?.length) {
|
|
const addr = addresses[0];
|
|
const parts = [
|
|
addr.addressLine1,
|
|
addr.townVillage,
|
|
addr.city,
|
|
addr.countyDistrict,
|
|
addr.addressLine3
|
|
].filter(Boolean);
|
|
if (parts.length) {
|
|
target.fullAddress = parts.join(', ') + (addr.postalCode ? ' - ' + addr.postalCode : '');
|
|
}
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
|
|
// Fetch profile image
|
|
this.clientService.getClientProfileImage(cid).subscribe({
|
|
next: (imageData: any) => {
|
|
if (imageData) {
|
|
target.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(imageData);
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
|
|
// Fetch signature image
|
|
this.clientService.getClientDocuments(cid).subscribe({
|
|
next: (docs: any[]) => {
|
|
const sigDoc = docs?.find(
|
|
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
|
);
|
|
if (sigDoc) {
|
|
this.clientService.getClientSignatureImage(cid, sigDoc.id.toString()).subscribe({
|
|
next: (blob: Blob) => {
|
|
target.signatureUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
|
|
},
|
|
error: () => {}
|
|
});
|
|
}
|
|
},
|
|
error: () => {}
|
|
});
|
|
}
|
|
|
|
/** Remove selected guardian with confirmation */
|
|
removeGuardian(): void {
|
|
const dialogRef = this.dialog.open(GuardianDeleteConfirmDialogComponent, {
|
|
width: '400px'
|
|
});
|
|
dialogRef.afterClosed().subscribe((confirmed: boolean) => {
|
|
if (confirmed) {
|
|
this.selectedGuardian = null;
|
|
this.guardianExpanded = false;
|
|
this.createClientForm.get('guardianDetails')?.patchValue({
|
|
guardianClientId: ''
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Dismiss guardian search dropdown on blur (delayed so click events still fire) */
|
|
dismissGuardianSearch(): void {
|
|
setTimeout(() => {
|
|
this.guardianSearchResults = [];
|
|
this.guardianSearching = false;
|
|
}, 200);
|
|
}
|
|
|
|
/** Dismiss signatory search dropdown on blur (delayed so click events still fire) */
|
|
dismissSignatorySearch(): void {
|
|
setTimeout(() => {
|
|
this.signatorySearchResults = [];
|
|
this.signatorySearching = false;
|
|
}, 200);
|
|
}
|
|
|
|
/** Search existing clients to add as authorized signatory */
|
|
onSignatorySearch(text: string): void {
|
|
this.signatorySearchText = text;
|
|
clearTimeout(this.signatorySearchTimer);
|
|
if (!text || text.length < 2) {
|
|
this.signatorySearchResults = [];
|
|
return;
|
|
}
|
|
this.signatorySearching = true;
|
|
this.signatorySearchTimer = setTimeout(() => {
|
|
this.clientService.searchByText(text, 0, 10, '', '', true).subscribe({
|
|
next: (data: any) => {
|
|
this.signatorySearchResults = data?.content || [];
|
|
this.signatorySearching = false;
|
|
this.clientService.enrichSearchResults(this.signatorySearchResults);
|
|
},
|
|
error: () => {
|
|
this.signatorySearchResults = [];
|
|
this.signatorySearching = false;
|
|
}
|
|
});
|
|
}, 400);
|
|
}
|
|
|
|
addSignatory(client: any): void {
|
|
// Prevent duplicates
|
|
if (this.signatories.find((s) => s.id === client.id)) {
|
|
this.signatorySearchResults = [];
|
|
this.signatorySearchText = '';
|
|
return;
|
|
}
|
|
const sig = { ...client, _loading: true };
|
|
this.signatories.push(sig);
|
|
this.signatorySearchResults = [];
|
|
this.signatorySearchText = '';
|
|
this.syncSignatoryNames();
|
|
|
|
this.fetchClientFullDetails(client.id, sig);
|
|
this.openDinVerifyDialog(sig);
|
|
}
|
|
|
|
/** Open DIN verification dialog for a signatory */
|
|
openDinVerifyDialog(sig: any): void {
|
|
const cid = sig.id?.toString();
|
|
if (!cid) return;
|
|
|
|
const entityName = this.createClientForm.get('fullname')?.value || '';
|
|
|
|
const openDialog = (kycId: string, panFromKyc?: string) => {
|
|
const ref = this.dialog.open(DinVerifyDialogComponent, {
|
|
width: '620px',
|
|
maxWidth: '95vw',
|
|
disableClose: false,
|
|
autoFocus: true,
|
|
data: {
|
|
clientId: cid,
|
|
kycId,
|
|
signatoryName: sig.displayName || '',
|
|
cifNumber: sig.cifNumber || sig.accountNumber || cid,
|
|
panNumber: sig.panNumber || panFromKyc || '-',
|
|
entityName
|
|
}
|
|
});
|
|
ref.afterClosed().subscribe((result: any) => {
|
|
if (result && !result.skipped) {
|
|
sig.dinNumber = result.dinNumber || '';
|
|
sig.dinVerified = result.dinVerified || false;
|
|
sig.directorName = result.directorName || '';
|
|
sig.dinStatus = result.dinStatus || '';
|
|
sig.dinDesignation = result.dinDesignation || '';
|
|
sig.companyMatch = result.companyMatch ?? null;
|
|
sig.matchedCompany = result.matchedCompany || '';
|
|
}
|
|
});
|
|
};
|
|
|
|
// Fetch KYC ID first, then open dialog
|
|
this.clientService.getClientKyc(cid).subscribe({
|
|
next: (kyc: any) => openDialog(kyc?.id?.toString() || '', kyc?.panNumber),
|
|
error: () => openDialog('')
|
|
});
|
|
}
|
|
|
|
removeSignatoryAt(index: number): void {
|
|
this.signatories.splice(index, 1);
|
|
this.syncSignatoryNames();
|
|
}
|
|
|
|
viewSignatoryDetails(sig: any): void {
|
|
this.dialog.open(SignatoryDetailDialogComponent, {
|
|
width: '720px',
|
|
maxWidth: '95vw',
|
|
data: sig,
|
|
panelClass: 'signatory-detail-panel',
|
|
autoFocus: false
|
|
});
|
|
}
|
|
|
|
private syncSignatoryNames(): void {
|
|
const names = this.signatories.map((s) => s.displayName).join(', ');
|
|
this.createClientForm.get('clientNonPersonDetails')?.patchValue({ authorizedSignatory: names });
|
|
}
|
|
|
|
/** Watch MSME turnover & investment for auto-classification */
|
|
/** Maps salutation name to gender. Returns gender option or null. */
|
|
private salutationGenderMap: Record<string, string> = {
|
|
mr: 'male',
|
|
'mr.': 'male',
|
|
shri: 'male',
|
|
sri: 'male',
|
|
master: 'male',
|
|
mrs: 'female',
|
|
'mrs.': 'female',
|
|
ms: 'female',
|
|
'ms.': 'female',
|
|
miss: 'female',
|
|
smt: 'female',
|
|
'smt.': 'female',
|
|
kumari: 'female'
|
|
// Dr, Er, CA, Adv etc. are gender-neutral — no auto-fill
|
|
};
|
|
|
|
private salutationSub: any;
|
|
|
|
/** Watch salutation changes and auto-fill gender */
|
|
private watchSalutationForGender() {
|
|
if (this.salutationSub) {
|
|
this.salutationSub.unsubscribe();
|
|
}
|
|
const salCtrl = this.createClientForm.get('salutationId');
|
|
if (!salCtrl) return;
|
|
|
|
this.salutationSub = salCtrl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((salutationId: any) => {
|
|
if (!salutationId || !this.salutationOptions?.length || !this.genderOptions?.length) return;
|
|
|
|
// Find the salutation name from the selected ID
|
|
const salutation = this.salutationOptions.find((s: any) => s.id === salutationId);
|
|
if (!salutation) return;
|
|
|
|
const salName = (salutation.name || '').toLowerCase().trim().replace(/\.$/, '');
|
|
const mappedGender = this.salutationGenderMap[salName] || this.salutationGenderMap[salName + '.'];
|
|
if (!mappedGender) return;
|
|
|
|
// Find matching gender option by name
|
|
const genderOption = this.genderOptions.find((g: any) => (g.name || '').toLowerCase().trim() === mappedGender);
|
|
|
|
if (genderOption) {
|
|
this.createClientForm.get('genderId')?.setValue(genderOption.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Returns capitalized full name preview from first + middle + last */
|
|
get fullNamePreview(): string {
|
|
const capitalize = (s: string) => (s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : '');
|
|
const salutationId = this.createClientForm?.get('salutationId')?.value;
|
|
const salutation = this.salutationOptions?.find((s: any) => s.id === salutationId)?.name || '';
|
|
const first = capitalize(this.createClientForm?.get('firstname')?.value || '');
|
|
const middle = capitalize(this.createClientForm?.get('middlename')?.value || '');
|
|
const last = capitalize(this.createClientForm?.get('lastname')?.value || '');
|
|
const full = [
|
|
first,
|
|
middle,
|
|
last
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
return salutation ? `${salutation} ${full}` : full;
|
|
}
|
|
|
|
getSelectedGender(): any {
|
|
const val = this.createClientForm.get('genderId')?.value;
|
|
return this.genderOptions?.find((g: any) => g.id === val) || null;
|
|
}
|
|
|
|
getGenderIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('male') && !n.includes('female')) return 'male';
|
|
if (n.includes('female')) return 'female';
|
|
if (n.includes('trans')) return 'transgender';
|
|
if (n.includes('other')) return 'wc';
|
|
return 'person';
|
|
}
|
|
|
|
getSelectedOccupationCategory(): any {
|
|
const val = this.createClientForm.get('occupationCategoryId')?.value;
|
|
return this.occupationCategoryOptions?.find((c: any) => c.id === val) || null;
|
|
}
|
|
|
|
getSelectedOccupation(): any {
|
|
const val = this.createClientForm.get('occupationId')?.value;
|
|
return this.occupationOptions?.find((o: any) => o.id === val) || null;
|
|
}
|
|
|
|
getOccupationCategoryIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('salaried') || n.includes('employed')) return 'badge';
|
|
if (n.includes('self') || n.includes('business')) return 'storefront';
|
|
if (n.includes('profession')) return 'work';
|
|
if (n.includes('agricultur') || n.includes('farm')) return 'agriculture';
|
|
if (n.includes('retire')) return 'elderly';
|
|
if (n.includes('student')) return 'school';
|
|
if (n.includes('home') || n.includes('house')) return 'home';
|
|
if (n.includes('government') || n.includes('govt') || n.includes('psu')) return 'account_balance';
|
|
if (n.includes('private')) return 'business';
|
|
if (n.includes('unemploy') || n.includes('not employ')) return 'person_off';
|
|
if (n.includes('other')) return 'more_horiz';
|
|
return 'work_outline';
|
|
}
|
|
|
|
getOccupationIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('doctor') || n.includes('medical') || n.includes('health')) return 'local_hospital';
|
|
if (n.includes('engineer')) return 'engineering';
|
|
if (n.includes('teacher') || n.includes('professor') || n.includes('education')) return 'school';
|
|
if (n.includes('lawyer') || n.includes('advocate') || n.includes('legal')) return 'gavel';
|
|
if (n.includes('account') || n.includes('ca ') || n.includes('chartered')) return 'calculate';
|
|
if (n.includes('architect')) return 'architecture';
|
|
if (n.includes('it ') || n.includes('software') || n.includes('tech') || n.includes('computer')) return 'computer';
|
|
if (n.includes('bank') || n.includes('financ') || n.includes('insurance')) return 'account_balance';
|
|
if (n.includes('police') || n.includes('army') || n.includes('defence') || n.includes('military')) return 'shield';
|
|
if (n.includes('driver') || n.includes('transport')) return 'local_shipping';
|
|
if (n.includes('shop') || n.includes('retail') || n.includes('trade')) return 'shopping_cart';
|
|
if (n.includes('farm') || n.includes('agricultur')) return 'agriculture';
|
|
if (n.includes('labour') || n.includes('worker') || n.includes('daily')) return 'construction';
|
|
if (n.includes('cook') || n.includes('chef') || n.includes('food') || n.includes('hotel')) return 'restaurant';
|
|
if (n.includes('tailor') || n.includes('textile') || n.includes('garment')) return 'checkroom';
|
|
if (n.includes('artist') || n.includes('media') || n.includes('journalist')) return 'palette';
|
|
if (n.includes('govt') || n.includes('government') || n.includes('civil serv')) return 'account_balance';
|
|
if (n.includes('retired') || n.includes('pension')) return 'elderly';
|
|
if (n.includes('student')) return 'school';
|
|
if (n.includes('housewife') || n.includes('homemaker') || n.includes('home')) return 'home';
|
|
if (n.includes('business') || n.includes('entrepreneur')) return 'storefront';
|
|
if (n.includes('other')) return 'more_horiz';
|
|
return 'work_outline';
|
|
}
|
|
|
|
getSelectedLegalFormLabel(): string {
|
|
const val = this.createClientForm.get('legalFormId')?.value;
|
|
return val === 1 ? 'Individual' : val === 2 ? 'Non-Individual' : '';
|
|
}
|
|
|
|
getSelectedNationality(): any {
|
|
const val = this.createClientForm.get('nationalityId')?.value;
|
|
return this.nationalityOptions.find((n) => n.value === val) || null;
|
|
}
|
|
|
|
getSelectedBusiness(): any {
|
|
const val = this.createClientForm.get('clientNonPersonDetails.mainBusinessLineId')?.value;
|
|
return this.businessLineOptions?.find((b: any) => b.id === val) || null;
|
|
}
|
|
|
|
getSelectedMaritalStatus(): any {
|
|
const val = this.createClientForm.get('maritalStatusId')?.value;
|
|
return this.maritalStatusOptions.find((ms: any) => ms.id === val) || null;
|
|
}
|
|
|
|
getMaritalStatusIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('single')) return 'person';
|
|
if (n.includes('married')) return 'favorite';
|
|
if (n.includes('unmarried')) return 'person_outline';
|
|
if (n.includes('divorced')) return 'heart_broken';
|
|
if (n.includes('widowed')) return 'sentiment_dissatisfied';
|
|
if (n.includes('separated')) return 'people_alt';
|
|
if (n.includes('annulled')) return 'remove_circle_outline';
|
|
if (n.includes('live-in') || n.includes('live in')) return 'home';
|
|
if (n.includes('not disclosed')) return 'visibility_off';
|
|
return 'help_outline';
|
|
}
|
|
|
|
getSelectedEducationLevel(): any {
|
|
const val = this.createClientForm.get('educationLevelId')?.value;
|
|
return this.educationLevelOptions.find((e: any) => e.id === val) || null;
|
|
}
|
|
|
|
getEducationLevelIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (
|
|
n.includes('school') ||
|
|
n.includes('10th') ||
|
|
n.includes('12th') ||
|
|
n.includes('ssc') ||
|
|
n.includes('hsc') ||
|
|
n.includes('matric') ||
|
|
n.includes('intermediate')
|
|
)
|
|
return 'school';
|
|
if (n.includes('diploma') || n.includes('iti')) return 'card_membership';
|
|
if (n.includes('under') || n.includes('bachelor') || (n.includes('graduate') && !n.includes('post')))
|
|
return 'menu_book';
|
|
if (n.includes('post') || n.includes('master')) return 'auto_stories';
|
|
if (n.includes('doctor') || n.includes('phd') || n.includes('ph.d')) return 'workspace_premium';
|
|
if (n.includes('profession')) return 'workspace_premium';
|
|
if (n.includes('none') || n.includes('illiter') || n.includes('below')) return 'block';
|
|
return 'school';
|
|
}
|
|
|
|
getSelectedDegreeQualification(): any {
|
|
const val = this.createClientForm.get('degreeQualificationId')?.value;
|
|
return this.degreeQualificationOptions.find((d: any) => d.id === val) || null;
|
|
}
|
|
|
|
/**
|
|
* Classifies an education level name into one of: school | undergraduate | postgraduate | doctorate.
|
|
* Used to filter the Degree/Qualification dropdown to relevant options.
|
|
*/
|
|
getEducationLevelCategory(name: string): string {
|
|
const n = (name || '').toLowerCase().replace(/[\s_-]+/g, '');
|
|
if (n.includes('doctor') || n.includes('phd') || n.includes('profession')) return 'doctorate';
|
|
if (n.includes('postgrad') || n === 'pg' || n.includes('master')) return 'postgraduate';
|
|
if (n.includes('undergrad') || n === 'ug' || n.includes('bachelor') || n.includes('graduate'))
|
|
return 'undergraduate';
|
|
if (
|
|
n.includes('school') ||
|
|
n.includes('10th') ||
|
|
n.includes('12th') ||
|
|
n.includes('ssc') ||
|
|
n.includes('hsc') ||
|
|
n.includes('matric') ||
|
|
n.includes('intermediate') ||
|
|
n.includes('diploma') ||
|
|
n.includes('iti') ||
|
|
n.includes('below')
|
|
)
|
|
return 'school';
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Classifies a degree / qualification name into the education level it belongs to.
|
|
*/
|
|
getDegreeQualificationCategory(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
// Doctorate / professional
|
|
if (
|
|
n.includes('ph.d') ||
|
|
n.includes('phd') ||
|
|
n.includes('d.litt') ||
|
|
n.includes('d.sc') ||
|
|
n.startsWith('ca ') ||
|
|
n === 'ca' ||
|
|
n.includes('chartered accountant') ||
|
|
n.startsWith('cs ') ||
|
|
n === 'cs' ||
|
|
n.includes('company secretary') ||
|
|
n.includes('cma') ||
|
|
n.includes('cfa') ||
|
|
n.includes('doctorate')
|
|
)
|
|
return 'doctorate';
|
|
// Postgraduate — starts with M. or contains master / PG-specific
|
|
if (
|
|
n.startsWith('m.') ||
|
|
n.startsWith('m ') ||
|
|
n.includes('master') ||
|
|
n.includes('mba') ||
|
|
n.includes('mca') ||
|
|
n.includes('mds') ||
|
|
n.includes('msw') ||
|
|
n.includes('mfa') ||
|
|
n.match(/\bmd\b/) ||
|
|
n.match(/\bms\b/)
|
|
)
|
|
return 'postgraduate';
|
|
// Undergraduate — starts with B. or contains bachelor / UG-specific
|
|
if (
|
|
n.startsWith('b.') ||
|
|
n.startsWith('b ') ||
|
|
n.includes('bachelor') ||
|
|
n.includes('bba') ||
|
|
n.includes('bca') ||
|
|
n.includes('mbbs') ||
|
|
n.includes('bds') ||
|
|
n.includes('bams') ||
|
|
n.includes('bhms') ||
|
|
n.includes('bfa') ||
|
|
n.includes('bhm') ||
|
|
n.includes('llb') ||
|
|
n.includes('nursing')
|
|
)
|
|
return 'undergraduate';
|
|
// School
|
|
if (
|
|
n.includes('10th') ||
|
|
n.includes('12th') ||
|
|
n.includes('ssc') ||
|
|
n.includes('hsc') ||
|
|
n.includes('matric') ||
|
|
n.includes('intermediate') ||
|
|
n.includes('diploma') ||
|
|
n.includes('iti') ||
|
|
n.includes('below')
|
|
)
|
|
return 'school';
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Degrees filtered by the currently selected Education Level.
|
|
* Returns all degrees if no level is selected, or category can't be determined.
|
|
*/
|
|
get filteredDegreeQualificationOptions(): any[] {
|
|
const level = this.getSelectedEducationLevel();
|
|
if (!level) return [];
|
|
const category = this.getEducationLevelCategory(level.name);
|
|
if (!category) return this.degreeQualificationOptions;
|
|
const filtered = this.degreeQualificationOptions.filter((d: any) => {
|
|
const c = this.getDegreeQualificationCategory(d.name);
|
|
return c === category || c === '';
|
|
});
|
|
return filtered.length > 0 ? filtered : this.degreeQualificationOptions;
|
|
}
|
|
|
|
getSelectedReligion(): any {
|
|
const val = this.createClientForm.get('religionId')?.value;
|
|
return this.religionOptions.find((r: any) => r.id === val) || null;
|
|
}
|
|
|
|
getReligionIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('hindu')) return 'temple_hindu';
|
|
if (n.includes('muslim') || n.includes('islam')) return 'mosque';
|
|
if (n.includes('christ')) return 'church';
|
|
if (n.includes('sikh')) return 'temple_buddhist';
|
|
if (n.includes('buddh')) return 'temple_buddhist';
|
|
if (n.includes('jain')) return 'temple_hindu';
|
|
if (n.includes('jew') || n.includes('juda')) return 'synagogue';
|
|
if (n.includes('parsi') || n.includes('zoroastrian')) return 'local_fire_department';
|
|
if (n.includes('bahai') || n.includes("baha'i")) return 'auto_awesome';
|
|
if (n.includes('none') || n.includes('atheist') || n.includes('not disclosed')) return 'visibility_off';
|
|
if (n.includes('other')) return 'more_horiz';
|
|
return 'public';
|
|
}
|
|
|
|
getSelectedSocialCategory(): any {
|
|
const val = this.createClientForm.get('socialCategoryId')?.value;
|
|
return this.socialCategoryOptions.find((s: any) => s.id === val) || null;
|
|
}
|
|
|
|
getSocialCategoryIcon(name: string): string {
|
|
const n = (name || '').toLowerCase().replace(/[\s.-]+/g, '');
|
|
if (n.includes('general') || n === 'gen' || n.includes('unreserved')) return 'groups';
|
|
if (n.includes('obc') || n.includes('otherbackward')) return 'diversity_3';
|
|
if (n === 'sc' || n.includes('scheduledcaste')) return 'diversity_1';
|
|
if (n === 'st' || n.includes('scheduledtribe')) return 'forest';
|
|
if (n.includes('ews') || n.includes('economically')) return 'savings';
|
|
if (n.includes('minority')) return 'diversity_2';
|
|
if (n.includes('other')) return 'more_horiz';
|
|
if (n.includes('notdisclosed')) return 'visibility_off';
|
|
return 'groups';
|
|
}
|
|
|
|
getDegreeQualificationIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (
|
|
n.includes('b.tech') ||
|
|
n.includes('m.tech') ||
|
|
n.includes('b.e') ||
|
|
n.includes('m.e') ||
|
|
n.includes('engineer')
|
|
)
|
|
return 'engineering';
|
|
if (
|
|
n.includes('mbbs') ||
|
|
n.includes('md') ||
|
|
n.includes('ms ') ||
|
|
n.includes('medic') ||
|
|
n.includes('bhms') ||
|
|
n.includes('bams')
|
|
)
|
|
return 'local_hospital';
|
|
if (n.includes('bds') || n.includes('mds') || n.includes('dental')) return 'medical_services';
|
|
if (n.includes('pharm')) return 'medication';
|
|
if (n.includes('b.com') || n.includes('m.com') || n.includes('commerce')) return 'trending_up';
|
|
if (n.includes('mba') || n.includes('bba') || n.includes('business')) return 'business_center';
|
|
if (n.includes('bca') || n.includes('mca') || n.includes('comput')) return 'computer';
|
|
if (n.includes('llb') || n.includes('llm') || n.includes('law')) return 'gavel';
|
|
if (n.includes('b.ed') || n.includes('m.ed') || n.includes('educat')) return 'cast_for_education';
|
|
if (n.includes('b.sc') || n.includes('m.sc') || n.includes('science')) return 'science';
|
|
if (n.includes('b.a') || n.includes('m.a') || n.includes('arts')) return 'palette';
|
|
if (n.includes('arch')) return 'architecture';
|
|
if (n.includes('nurs')) return 'medical_information';
|
|
if (n.includes('ca ') || n.includes('chartered')) return 'calculate';
|
|
if (n.includes('cs ') || n.includes('company secretary')) return 'description';
|
|
if (n.includes('cfa') || n.includes('cma')) return 'pie_chart';
|
|
if (n.includes('phd') || n.includes('ph.d') || n.includes('doctorate')) return 'workspace_premium';
|
|
if (n.includes('10th') || n.includes('ssc') || n.includes('matric')) return 'menu_book';
|
|
if (n.includes('12th') || n.includes('hsc') || n.includes('intermediate')) return 'auto_stories';
|
|
if (n.includes('diploma') || n.includes('iti')) return 'card_membership';
|
|
if (n.includes('other')) return 'more_horiz';
|
|
return 'school';
|
|
}
|
|
|
|
getBusinessIcon(name: string): string {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('manufactur')) return 'precision_manufacturing';
|
|
if (n.includes('trad')) return 'storefront';
|
|
if (n.includes('service')) return 'support_agent';
|
|
if (n.includes('agricult') || n.includes('farm')) return 'agriculture';
|
|
if (n.includes('construct') || n.includes('real estate')) return 'construction';
|
|
if (n.includes('retail')) return 'shopping_cart';
|
|
if (n.includes('transport') || n.includes('logistic')) return 'local_shipping';
|
|
if (n.includes('health') || n.includes('pharma') || n.includes('medical')) return 'local_hospital';
|
|
if (n.includes('education') || n.includes('school')) return 'school';
|
|
if (n.includes('tech') || n.includes('it') || n.includes('software')) return 'computer';
|
|
if (n.includes('food') || n.includes('restaurant') || n.includes('hotel')) return 'restaurant';
|
|
if (n.includes('finance') || n.includes('bank') || n.includes('insurance')) return 'account_balance';
|
|
if (n.includes('textile') || n.includes('garment')) return 'checkroom';
|
|
if (n.includes('mining') || n.includes('mineral')) return 'landscape';
|
|
if (n.includes('energy') || n.includes('power') || n.includes('electric')) return 'bolt';
|
|
if (n.includes('media') || n.includes('entertainment')) return 'movie';
|
|
if (n.includes('telecom') || n.includes('communication')) return 'cell_tower';
|
|
return 'business';
|
|
}
|
|
|
|
getDateLabel(legalFormId: number, values: string[]): string {
|
|
return legalFormId === LegalFormId.PERSON ? values[0] : values[1];
|
|
}
|
|
|
|
/**
|
|
* Client General Details
|
|
*/
|
|
get clientGeneralDetails() {
|
|
const generalDetails = this.readOnly ? this.createClientForm.getRawValue() : this.createClientForm.value;
|
|
const dateFormat = this.settingsService.dateFormat;
|
|
const locale = this.settingsService.language.code;
|
|
const isEditMode = !!this.clientData;
|
|
for (const key in generalDetails) {
|
|
if (key === 'addSavings') {
|
|
delete generalDetails[key];
|
|
continue;
|
|
}
|
|
if (generalDetails[key] === '') {
|
|
// In edit mode, preserve empty strings for fields that previously had a value so the
|
|
// backend can clear them (Fineract treats a missing field as "no change" but an empty
|
|
// string as a deliberate clear).
|
|
if (isEditMode && this.clientData[key] != null && this.clientData[key] !== '') {
|
|
continue;
|
|
}
|
|
delete generalDetails[key];
|
|
}
|
|
}
|
|
if (generalDetails.submittedOnDate instanceof Date) {
|
|
generalDetails.submittedOnDate = this.dateUtils.formatDate(generalDetails.submittedOnDate, dateFormat);
|
|
}
|
|
if (generalDetails.activationDate instanceof Date) {
|
|
generalDetails.activationDate = this.dateUtils.formatDate(generalDetails.activationDate, dateFormat);
|
|
}
|
|
if (generalDetails.dateOfBirth instanceof Date) {
|
|
generalDetails.dateOfBirth = this.dateUtils.formatDate(generalDetails.dateOfBirth, dateFormat);
|
|
}
|
|
|
|
if (generalDetails.clientNonPersonDetails) {
|
|
generalDetails.clientNonPersonDetails = {
|
|
...generalDetails.clientNonPersonDetails,
|
|
dateFormat,
|
|
locale
|
|
};
|
|
}
|
|
// Add computed age breakdown from dateOfBirth
|
|
if (this.ageBreakdown.years > 0 || this.ageBreakdown.months > 0 || this.ageBreakdown.days > 0) {
|
|
generalDetails.ageYears = this.ageBreakdown.years;
|
|
generalDetails.ageMonths = this.ageBreakdown.months;
|
|
generalDetails.ageDays = this.ageBreakdown.days;
|
|
}
|
|
|
|
// Store extended fields separately (not yet supported by Fineract API)
|
|
// Add isMinor flag
|
|
generalDetails.isMinor = this.isMinor;
|
|
|
|
// Add guardian clientId for minors
|
|
if (this.isMinor && generalDetails.guardianDetails) {
|
|
generalDetails.guardianClientId = generalDetails.guardianDetails.guardianClientId;
|
|
generalDetails.guardianRelation = generalDetails.guardianDetails.guardianRelation;
|
|
}
|
|
|
|
// Add DIP verification status
|
|
generalDetails.dipVerified = this.mobileVerified;
|
|
if (this.dipLookupLogId) {
|
|
generalDetails.dipLookupLogId = this.dipLookupLogId;
|
|
}
|
|
if (this.dipTimestamp) {
|
|
generalDetails.dipTimestamp = this.dipTimestamp;
|
|
}
|
|
|
|
// Add alternate mobile DIP verification status
|
|
generalDetails.alternateDipVerified = this.alternateMobileVerified;
|
|
if (this.alternateDipLookupLogId) {
|
|
generalDetails.alternateDipLookupLogId = this.alternateDipLookupLogId;
|
|
}
|
|
if (this.alternateDipTimestamp) {
|
|
generalDetails.alternateDipTimestamp = this.alternateDipTimestamp;
|
|
}
|
|
|
|
// Add name verification (sanctions screening) status
|
|
generalDetails.nameVerified = this.sanctionsVerified;
|
|
|
|
// Add sanctions screening log ID
|
|
if (this.screeningLogId) {
|
|
generalDetails.screeningLogId = this.screeningLogId;
|
|
}
|
|
|
|
const unsupportedFields = [
|
|
'guardianDetails',
|
|
'addressDetails',
|
|
'entityKycDetails'
|
|
];
|
|
unsupportedFields.forEach((f) => delete generalDetails[f]);
|
|
|
|
// Format dateOfRegistration if present
|
|
if (generalDetails.clientNonPersonDetails?.dateOfRegistration instanceof Date) {
|
|
generalDetails.clientNonPersonDetails.dateOfRegistration = this.dateUtils.formatDate(
|
|
generalDetails.clientNonPersonDetails.dateOfRegistration,
|
|
dateFormat
|
|
);
|
|
}
|
|
|
|
return generalDetails;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Signatory / Guardian Detail Dialog
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-signatory-detail-dialog',
|
|
template: `
|
|
<div class="sd">
|
|
<!-- Header -->
|
|
<div class="sd-header">
|
|
<div class="sd-avatar" [class.sd-avatar--photo]="data.profileImageUrl">
|
|
@if (data.profileImageUrl) {
|
|
<img [src]="data.profileImageUrl" alt="Photo" />
|
|
} @else {
|
|
<fa-icon icon="user-shield" class="sd-avatar-icon"></fa-icon>
|
|
}
|
|
</div>
|
|
<div class="sd-header-info">
|
|
<h3 class="sd-name">{{ data.displayName }}</h3>
|
|
<p class="sd-sub">CIF: {{ data.cifNumber || data.id }} · {{ data.status?.value || 'Active' }}</p>
|
|
</div>
|
|
<button mat-icon-button [mat-dialog-close]="true" class="sd-close"><mat-icon>close</mat-icon></button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<mat-dialog-content class="sd-body">
|
|
@if (loading) {
|
|
<div class="sd-loading">
|
|
<mat-progress-spinner diameter="24" mode="indeterminate"></mat-progress-spinner>
|
|
<span>Loading details...</span>
|
|
</div>
|
|
}
|
|
<!-- Account Info -->
|
|
<div class="sd-section">
|
|
<div class="sd-row">
|
|
<div class="sd-field">
|
|
<span class="sd-label">Customer ID</span>
|
|
<span class="sd-val">{{ data.cifNumber || data.id }}</span>
|
|
</div>
|
|
<div class="sd-field">
|
|
<span class="sd-label">Account No</span>
|
|
<span class="sd-val">{{ data.accountNo || data.accountNumber || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="sd-row">
|
|
<div class="sd-field">
|
|
<span class="sd-label">Account Type</span>
|
|
<span class="sd-val">{{ data.accountType || '-' }}</span>
|
|
</div>
|
|
<div class="sd-field">
|
|
<span class="sd-label">Office</span>
|
|
<span class="sd-val">{{ data.officeName || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Address -->
|
|
<div class="sd-section">
|
|
<div class="sd-row">
|
|
<div class="sd-field sd-field--full">
|
|
<span class="sd-label"><mat-icon class="sd-icon">location_on</mat-icon> Address</span>
|
|
<span class="sd-val">{{ data.fullAddress || 'No address on record' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- KYC -->
|
|
<div class="sd-section sd-section--kyc">
|
|
<div class="sd-row">
|
|
<div class="sd-field">
|
|
<span class="sd-label"><mat-icon class="sd-icon">credit_card</mat-icon> PAN</span>
|
|
<span class="sd-val sd-val--mono">
|
|
{{ data.panNumber || '-' }}
|
|
@if (data.panVerified) {
|
|
<mat-icon class="sd-tick">check_circle</mat-icon>
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="sd-field">
|
|
<span class="sd-label"><mat-icon class="sd-icon">fingerprint</mat-icon> Aadhaar</span>
|
|
<span class="sd-val sd-val--mono">
|
|
{{ data.aadhaarMasked || '-' }}
|
|
@if (data.aadhaarVerified) {
|
|
<mat-icon class="sd-tick">check_circle</mat-icon>
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact -->
|
|
@if (data.mobileNo || data.emailAddress) {
|
|
<div class="sd-section">
|
|
<div class="sd-row">
|
|
@if (data.mobileNo) {
|
|
<div class="sd-field">
|
|
<span class="sd-label"><mat-icon class="sd-icon">phone_android</mat-icon> Mobile</span>
|
|
<span class="sd-val">{{ data.mobileNo }}</span>
|
|
</div>
|
|
}
|
|
@if (data.emailAddress) {
|
|
<div class="sd-field">
|
|
<span class="sd-label"><mat-icon class="sd-icon">email</mat-icon> Email</span>
|
|
<span class="sd-val">{{ data.emailAddress }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Signature -->
|
|
@if (data.signatureUrl) {
|
|
<div class="sd-section sd-section--sig">
|
|
<span class="sd-label"><mat-icon class="sd-icon">draw</mat-icon> Specimen Signature</span>
|
|
<div class="sd-sig-frame">
|
|
<img [src]="data.signatureUrl" alt="Signature" />
|
|
</div>
|
|
</div>
|
|
}
|
|
</mat-dialog-content>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
:host {
|
|
--bg: #f0f4f8;
|
|
--surface: #ffffff;
|
|
--border: #e2e8f0;
|
|
--border-input: #cbd5e1;
|
|
--primary: #1e6bb8;
|
|
--primary-light: #ebf3ff;
|
|
--text: #1a2332;
|
|
--text-muted: #64748b;
|
|
--radius-card: 12px;
|
|
}
|
|
|
|
.sd {
|
|
background: var(--bg);
|
|
border-radius: var(--radius-card);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Header */
|
|
.sd-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 20px 24px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.sd-avatar {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
background: var(--primary-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
border: 2px solid var(--border);
|
|
}
|
|
.sd-avatar--photo {
|
|
border-color: var(--primary);
|
|
}
|
|
.sd-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.sd-avatar-icon {
|
|
font-size: 22px;
|
|
color: var(--primary);
|
|
}
|
|
.sd-header-info {
|
|
flex: 1;
|
|
}
|
|
.sd-name {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
.sd-sub {
|
|
margin: 2px 0 0;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
}
|
|
.sd-close {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Body */
|
|
.sd-body {
|
|
padding: 0 !important;
|
|
background: var(--bg);
|
|
max-height: none !important;
|
|
overflow: visible !important;
|
|
padding-bottom: 16px !important;
|
|
}
|
|
.sd-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 20px 24px;
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* Sections */
|
|
.sd-section {
|
|
background: var(--surface);
|
|
margin: 12px 16px;
|
|
padding: 16px 20px;
|
|
border-radius: var(--radius-card);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.sd-section--kyc {
|
|
background: var(--primary-light);
|
|
border-color: #c7ddf5;
|
|
}
|
|
.sd-section--sig {
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.sd-row {
|
|
display: flex;
|
|
gap: 24px;
|
|
}
|
|
.sd-row + .sd-row {
|
|
margin-top: 14px;
|
|
}
|
|
.sd-field {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.sd-field--full {
|
|
flex: 1 1 100%;
|
|
}
|
|
|
|
.sd-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
letter-spacing: 0.6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.sd-val {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.sd-val--mono {
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.sd-icon {
|
|
font-size: 15px !important;
|
|
width: 15px !important;
|
|
height: 15px !important;
|
|
color: var(--primary);
|
|
}
|
|
.sd-tick {
|
|
font-size: 18px !important;
|
|
width: 18px !important;
|
|
height: 18px !important;
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* Signature frame */
|
|
.sd-sig-frame {
|
|
margin-top: 8px;
|
|
padding: 12px;
|
|
background: var(--surface);
|
|
border: 1px dashed var(--border-input);
|
|
border-radius: 8px;
|
|
display: inline-block;
|
|
}
|
|
.sd-sig-frame img {
|
|
max-width: 300px;
|
|
max-height: 100px;
|
|
display: block;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.sd-row {
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
}
|
|
`
|
|
],
|
|
imports: [
|
|
STANDALONE_SHARED_IMPORTS,
|
|
MatIcon,
|
|
FaIconComponent,
|
|
MatDialogContent,
|
|
MatDialogClose,
|
|
MatProgressSpinner
|
|
]
|
|
})
|
|
export class SignatoryDetailDialogComponent {
|
|
private clientService = inject(ClientsService);
|
|
private sanitizer = inject(DomSanitizer);
|
|
private inputData = inject<any>(MAT_DIALOG_DATA);
|
|
data: any;
|
|
loading = false;
|
|
|
|
constructor() {
|
|
const inputData = this.inputData;
|
|
this.data = { ...inputData };
|
|
const cid = inputData.id?.toString();
|
|
if (!cid) return;
|
|
|
|
// Fetch profile image
|
|
this.clientService.getClientProfileImage(cid).subscribe({
|
|
next: (img: any) => {
|
|
if (img) {
|
|
this.data = { ...this.data, profileImageUrl: this.sanitizer.bypassSecurityTrustResourceUrl(img) };
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fetch signature
|
|
this.clientService.getClientDocuments(cid).subscribe({
|
|
next: (docs: any[]) => {
|
|
const sigDoc = docs?.find(
|
|
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
|
);
|
|
if (sigDoc) {
|
|
this.clientService.getClientSignatureImage(cid, sigDoc.id.toString()).subscribe({
|
|
next: (blob: Blob) => {
|
|
this.data = {
|
|
...this.data,
|
|
signatureUrl: this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob))
|
|
};
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Guardian Image Zoom Dialog
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-guardian-image-zoom-dialog',
|
|
template: `
|
|
<div class="zoom-dialog">
|
|
<div class="zoom-dialog__header">
|
|
<span class="zoom-dialog__title">{{ data.title }}</span>
|
|
<button mat-icon-button [mat-dialog-close]="true" class="zoom-dialog__close">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
<div class="zoom-dialog__body">
|
|
<img [src]="data.imageUrl" alt="{{ data.title }}" class="zoom-dialog__img" />
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
.zoom-dialog {
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.zoom-dialog__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
background: #0f172a;
|
|
color: #fff;
|
|
}
|
|
.zoom-dialog__title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
.zoom-dialog__close {
|
|
color: rgba(255, 255, 255, 0.7) !important;
|
|
}
|
|
.zoom-dialog__close:hover {
|
|
color: #fff !important;
|
|
}
|
|
.zoom-dialog__body {
|
|
padding: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f8fafc;
|
|
}
|
|
.zoom-dialog__img {
|
|
max-width: 100%;
|
|
max-height: 70vh;
|
|
object-fit: contain;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
}
|
|
`
|
|
],
|
|
imports: [
|
|
MatIcon,
|
|
MatDialogClose
|
|
]
|
|
})
|
|
export class GuardianImageZoomDialogComponent {
|
|
data = inject<{ imageUrl: string; title: string }>(MAT_DIALOG_DATA);
|
|
}
|
|
|
|
/**
|
|
* Guardian Delete Confirmation Dialog
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-guardian-delete-confirm-dialog',
|
|
template: `
|
|
<div class="gdc">
|
|
<div class="gdc-header">
|
|
<mat-icon class="gdc-header__icon">warning</mat-icon>
|
|
<h3 class="gdc-header__title">Remove Guardian</h3>
|
|
</div>
|
|
<div class="gdc-body">
|
|
<p>Are you sure you want to remove the linked guardian? This action cannot be undone.</p>
|
|
</div>
|
|
<div class="gdc-actions">
|
|
<button mat-stroked-button [mat-dialog-close]="false">Cancel</button>
|
|
<button mat-flat-button color="warn" [mat-dialog-close]="true">
|
|
<mat-icon>delete</mat-icon>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
.gdc {
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.gdc-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 16px 20px;
|
|
background: #fef2f2;
|
|
border-bottom: 1px solid #fecaca;
|
|
}
|
|
.gdc-header__icon {
|
|
color: #dc2626;
|
|
font-size: 24px;
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
.gdc-header__title {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #991b1b;
|
|
}
|
|
.gdc-body {
|
|
padding: 20px;
|
|
}
|
|
.gdc-body p {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
color: #374151;
|
|
line-height: 1.5;
|
|
}
|
|
.gdc-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
padding: 12px 20px;
|
|
border-top: 1px solid #f1f5f9;
|
|
}
|
|
.gdc-actions button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
`
|
|
],
|
|
imports: [
|
|
MatIcon,
|
|
MatDialogClose
|
|
]
|
|
})
|
|
export class GuardianDeleteConfirmDialogComponent {}
|