Merge remote-tracking branch 'origin/uate' into uate
This commit is contained in:
+86
-1
@@ -516,6 +516,51 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets transient session state back to a blank-client baseline.
|
||||
*
|
||||
* This app's RouteReusableStrategy (core/route/route-reusable-strategy.ts) keeps the SAME
|
||||
* component instance alive across repeat visits to /clients/create — `ngOnInit`/the
|
||||
* constructor never re-run for a second "create new client" session. Without this, computed
|
||||
* state like `isMinor`, the linked guardian, or a prior sanctions/OTP verification would leak
|
||||
* from whatever client was created in the previous session into the new one. Called by
|
||||
* CreateClientComponent whenever `route.data` re-emits (which happens on every navigation,
|
||||
* even a reused one).
|
||||
*/
|
||||
resetForNewClient(): void {
|
||||
this.createClientForm.reset();
|
||||
// legalFormId's raw FormControl default is '' — the buildDependencies() subscription
|
||||
// treats any non-PERSON value (including '') as "non-individual" and adds fullname /
|
||||
// clientNonPersonDetails. Patch it back to the same PERSON default buildDependencies()
|
||||
// itself applies on first construction, so a reset lands on a blank Individual client
|
||||
// instead of stuck with leftover Entity-only controls.
|
||||
this.createClientForm.get('legalFormId')?.setValue(LegalFormId.PERSON);
|
||||
this.isMinor = false;
|
||||
this.calculatedAgeDetail = null;
|
||||
this.ageBreakdown = { years: 0, months: 0, days: 0 };
|
||||
this.selectedGuardian = null;
|
||||
this.guardianSearchText = '';
|
||||
this.guardianSearchResults = [];
|
||||
this.guardianExpanded = false;
|
||||
if (this.createClientForm.get('guardianDetails')) {
|
||||
this.createClientForm.removeControl('guardianDetails');
|
||||
}
|
||||
// Verification state must not carry over — a previous client's cleared sanctions
|
||||
// screening or verified OTP must not silently apply to a different new client.
|
||||
this.mobileVerified = false;
|
||||
this.dipLookupLogId = null;
|
||||
this.dipTimestamp = null;
|
||||
this.alternateMobileVerified = false;
|
||||
this.alternateDipLookupLogId = null;
|
||||
this.alternateDipTimestamp = null;
|
||||
this.mobileNewOtpId = null;
|
||||
this.emailNewOtpId = null;
|
||||
this.sanctionsVerified = false;
|
||||
this.sanctionsBlocked = false;
|
||||
this.screeningLogId = null;
|
||||
this.verifiedName = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fills the General step form from a manually-entered family member
|
||||
* when the user clicks "Create as Client" on the family members tab.
|
||||
@@ -571,7 +616,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
mobileNo: data.mobileNo,
|
||||
alternateMobileNo: data.alternateMobileNo || '',
|
||||
emailAddress: data.emailAddress,
|
||||
dateOfBirth: data.dateOfBirth ? new Date(data.dateOfBirth) : '',
|
||||
dateOfBirth: data.dateOfBirth
|
||||
? Array.isArray(data.dateOfBirth)
|
||||
? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2])
|
||||
: new Date(data.dateOfBirth)
|
||||
: '',
|
||||
occupationCategoryId: data.occupationCategory?.id || data.occupationCategoryId || '',
|
||||
occupationId: data.occupation?.id || data.occupationId || '',
|
||||
monthlyIncome: data.monthlyIncome || '',
|
||||
@@ -653,6 +702,42 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore linked guardian for minor clients. The `guardianDetails` control group above was
|
||||
// just created synchronously by the dateOfBirth subscription (patched a few lines up), which
|
||||
// also resets `selectedGuardian` to null — so this has to run after that patch, not before it.
|
||||
// If a guardian is linked in the database (or the client is marked minor), we force minor state
|
||||
// and explicitly ensure the form control exists so the guardian details load correctly.
|
||||
if ((data.isMinor || this.isMinor || data.guardianClientId) && data.guardianClientId) {
|
||||
this.isMinor = true;
|
||||
if (!this.createClientForm.get('guardianDetails')) {
|
||||
this.createClientForm.addControl(
|
||||
'guardianDetails',
|
||||
this.formBuilder.group({
|
||||
guardianClientId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
guardianRelation: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.createClientForm.get('guardianDetails')?.patchValue({
|
||||
guardianClientId: data.guardianClientId,
|
||||
guardianRelation: data.guardianRelation || ''
|
||||
});
|
||||
this.selectedGuardian = {
|
||||
id: data.guardianClientId,
|
||||
displayName: data.guardianClientDisplayName || '',
|
||||
_loading: true
|
||||
};
|
||||
this.fetchClientFullDetails(data.guardianClientId, this.selectedGuardian);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+32
-1
@@ -43,6 +43,9 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
@Input() clientId: string = '';
|
||||
/** Read-only mode — disables form editing until user clicks edit icon */
|
||||
@Input() readOnly = false;
|
||||
/** Minor clients can't hold utility bills, bank statements or an adult-only OVD in their own
|
||||
* name — restricts the address-proof picker to Aadhaar/Passport when true. */
|
||||
@Input() isMinor = false;
|
||||
|
||||
activeTab = 0;
|
||||
sameAsPermanent = false;
|
||||
@@ -240,7 +243,9 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
this.clientsService.getKycProofTypes().subscribe({
|
||||
next: (res: any) => {
|
||||
const activeFilter = (p: any) => p?.active !== false;
|
||||
this.addressProofs = (res?.addressProofs || []).filter(activeFilter).map(normalizeProofType);
|
||||
this.addressProofs = this.filterAddressProofsForMinor(
|
||||
(res?.addressProofs || []).filter(activeFilter).map(normalizeProofType)
|
||||
);
|
||||
this.addressGroups = groupProofsByCategory(this.addressProofs);
|
||||
this.loadingProofs = false;
|
||||
},
|
||||
@@ -250,6 +255,21 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
/** Minors don't hold utility bills, bank statements, rent agreements, property tax receipts,
|
||||
* employer letters or pension orders in their own name, and Voter ID/Driving Licence both
|
||||
* require adult age — Aadhaar and Passport are the only OVDs left for proof of address. */
|
||||
private static readonly MINOR_ELIGIBLE_ADDRESS_PROOF_CODES = new Set([
|
||||
'AADHAAR',
|
||||
'PASSPORT'
|
||||
]);
|
||||
|
||||
private filterAddressProofsForMinor(proofs: ProofType[]): ProofType[] {
|
||||
if (!this.isMinor) return proofs;
|
||||
return proofs.filter((p) =>
|
||||
ClientIndiaAddressStepComponent.MINOR_ELIGIBLE_ADDRESS_PROOF_CODES.has((p.code || '').toUpperCase())
|
||||
);
|
||||
}
|
||||
|
||||
selectAddrProof(proof: ProofType): void {
|
||||
this.selectedAddrProof = proof;
|
||||
this.permanentForm.get('proofId')?.setValue(proof.id);
|
||||
@@ -355,6 +375,17 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changes['isMinor'] && !changes['isMinor'].firstChange && this.addressProofs.length) {
|
||||
const filtered = this.filterAddressProofsForMinor(this.addressProofs);
|
||||
if (filtered.length !== this.addressProofs.length) {
|
||||
this.addressProofs = filtered;
|
||||
this.addressGroups = groupProofsByCategory(this.addressProofs);
|
||||
if (this.selectedAddrProof && !filtered.some((p) => p.id === this.selectedAddrProof?.id)) {
|
||||
this.selectedAddrProof = null;
|
||||
this.permanentForm.get('proofId')?.setValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,33 +28,36 @@
|
||||
<div class="form-grid">
|
||||
<!-- ══ INDIVIDUAL KYC ══ -->
|
||||
@if (!isNonIndividual) {
|
||||
@if (readOnly) {
|
||||
<!-- In view mode the mat-checkbox renders as a faded grey square
|
||||
that reads as "blurry". Show a clean Yes/No pill instead. -->
|
||||
<div class="form-field checkbox-field full-width">
|
||||
<div class="ro-flag">
|
||||
<span class="ro-flag__label">Client has PAN Card</span>
|
||||
@if (kycForm.get('hasPan')?.value) {
|
||||
<span class="ro-flag__chip ro-flag__chip--yes">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Yes
|
||||
</span>
|
||||
} @else {
|
||||
<span class="ro-flag__chip ro-flag__chip--no">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
No
|
||||
</span>
|
||||
}
|
||||
<!-- Minors don't hold a PAN card — skip the PAN checkbox/section entirely -->
|
||||
@if (!isMinor) {
|
||||
@if (readOnly) {
|
||||
<!-- In view mode the mat-checkbox renders as a faded grey square
|
||||
that reads as "blurry". Show a clean Yes/No pill instead. -->
|
||||
<div class="form-field checkbox-field full-width">
|
||||
<div class="ro-flag">
|
||||
<span class="ro-flag__label">Client has PAN Card</span>
|
||||
@if (kycForm.get('hasPan')?.value) {
|
||||
<span class="ro-flag__chip ro-flag__chip--yes">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Yes
|
||||
</span>
|
||||
} @else {
|
||||
<span class="ro-flag__chip ro-flag__chip--no">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
No
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="form-field checkbox-field full-width">
|
||||
<mat-checkbox formControlName="hasPan">Client has PAN Card</mat-checkbox>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="form-field checkbox-field full-width">
|
||||
<mat-checkbox formControlName="hasPan">Client has PAN Card</mat-checkbox>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Individual PAN with inline verify -->
|
||||
@if (kycForm.get('hasPan')?.value) {
|
||||
@if (!isMinor && kycForm.get('hasPan')?.value) {
|
||||
<div class="form-field verify-field">
|
||||
<mat-form-field appearance="outline" class="verify-input">
|
||||
<mat-label>PAN Number</mat-label>
|
||||
@@ -1266,11 +1269,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ SECTION: Form 60/61 ═══ -->
|
||||
<!-- ═══ SECTION: Form 97/98 ═══ -->
|
||||
<div class="section-block">
|
||||
<h4 class="section-title">
|
||||
<fa-icon icon="file-invoice" class="section-icon"></fa-icon>
|
||||
Form 60 / 61
|
||||
Form 97 / 98
|
||||
@if (!kycForm.get('hasPan')?.value) {
|
||||
<span class="kyc-badge required-badge">Required — No PAN</span>
|
||||
}
|
||||
@@ -1278,14 +1281,14 @@
|
||||
@if (!kycForm.get('hasPan')?.value) {
|
||||
<div class="kyc-info-hint warning-hint">
|
||||
<mat-icon class="kyc-info-hint-icon">warning</mat-icon>
|
||||
<span>Client does not have PAN. Form 60/61 is mandatory.</span>
|
||||
<span>Client does not have PAN. Form 97/98 is mandatory.</span>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>Form Type</mat-label>
|
||||
<mat-select formControlName="form60Type">
|
||||
<mat-option value="FORM_60">Form 60</mat-option>
|
||||
<mat-option value="FORM_61">Form 61</mat-option>
|
||||
<mat-option value="FORM_60">Form 97</mat-option>
|
||||
<mat-option value="FORM_61">Form 98</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="form-field" (click)="form60DatePicker.open()">
|
||||
@@ -1296,26 +1299,26 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="form60-not-needed">Client has PAN — Form 60/61 not required.</p>
|
||||
<p class="form60-not-needed">Client has PAN — Form 97/98 not required.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- ═══ SECTION: Form 15G / 15H ═══ -->
|
||||
<!-- ═══ SECTION: Form 121 ═══ -->
|
||||
<div class="section-block">
|
||||
<h4 class="section-title">
|
||||
<fa-icon icon="receipt" class="section-icon"></fa-icon>
|
||||
Form 15G / 15H
|
||||
Form 121
|
||||
</h4>
|
||||
<div class="form-grid">
|
||||
<div class="form-field checkbox-field">
|
||||
<mat-checkbox formControlName="form15Applicable">Form 15G/15H Applicable</mat-checkbox>
|
||||
<mat-checkbox formControlName="form15Applicable">Form 121 Applicable</mat-checkbox>
|
||||
</div>
|
||||
@if (kycForm.get('form15Applicable')?.value) {
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>Form Type</mat-label>
|
||||
<mat-select formControlName="form15Type">
|
||||
<mat-option value="15G">15G (Below 60 years)</mat-option>
|
||||
<mat-option value="15H">15H (Senior Citizen 60+)</mat-option>
|
||||
<mat-option value="15G">Form 121 (Below 60 years)</mat-option>
|
||||
<mat-option value="15H">Form 121 (Senior Citizen 60+)</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
|
||||
@@ -61,6 +61,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Set by parent after client is created */
|
||||
@Input() clientId: string = '';
|
||||
@Input() isNonIndividual = false;
|
||||
/** Minor clients don't hold a PAN card — hides the PAN section on the individual KYC form */
|
||||
@Input() isMinor = false;
|
||||
/** Constitution metadata (panLabel, showCin, etc.) passed from general step */
|
||||
@Input() constitutionMeta: any = null;
|
||||
/** Entity full name (non-individual) — used for enterprise PAN verification */
|
||||
@@ -163,7 +165,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
{ name: 'Driving Licence', code: 'DRIVING_LICENSE' },
|
||||
{ name: 'Voter ID', code: 'VOTER_ID' },
|
||||
{ name: 'NREGA Job Card', code: 'NREGA_JOB_CARD' },
|
||||
{ name: 'National Population Register Letter', code: 'NPR_LETTER' }
|
||||
{ name: 'National Population Register Letter', code: 'NPR_LETTER' },
|
||||
{ name: 'Birth Certificate', code: 'BIRTH_CERTIFICATE' },
|
||||
{ name: 'School ID Card', code: 'SCHOOL_ID_CARD' }
|
||||
];
|
||||
/** Constitution-based KYC document types for non-individual clients */
|
||||
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
|
||||
@@ -458,15 +462,44 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
form15FY: ['']
|
||||
});
|
||||
|
||||
/** RBI OVDs a minor cannot hold: Voter ID and Driving Licence require adult age,
|
||||
* PAN and NREGA job cards are issued to adults. Aadhaar, Passport and NPR letters
|
||||
* are unaffected by age and stay available. */
|
||||
private static readonly MINOR_INELIGIBLE_DOC_KEYWORDS = [
|
||||
'pan',
|
||||
'driving',
|
||||
'voter',
|
||||
'nrega'
|
||||
];
|
||||
|
||||
/** Birth Certificate / School ID Card are minor-specific OVDs — not relevant once the
|
||||
* client is an adult, so they're hidden the other direction. */
|
||||
private static readonly MINOR_ONLY_DOC_CODES = new Set([
|
||||
'BIRTH_CERTIFICATE',
|
||||
'SCHOOL_ID_CARD'
|
||||
]);
|
||||
|
||||
private filterDocTypesForMinor<T extends { name: string; code?: string }>(docs: T[]): T[] {
|
||||
if (this.isMinor) {
|
||||
return docs.filter((d) => {
|
||||
const text = `${d.name || ''} ${d.code || ''}`.toLowerCase();
|
||||
return !ClientKycStepComponent.MINOR_INELIGIBLE_DOC_KEYWORDS.some((k) => text.includes(k));
|
||||
});
|
||||
}
|
||||
return docs.filter((d) => !ClientKycStepComponent.MINOR_ONLY_DOC_CODES.has((d.code || '').toUpperCase()));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Seed defaults immediately so the picker has options before clientId is set
|
||||
// (template API requires clientId; user can still pick a type pre-save).
|
||||
if (!this.kycDocumentTypes.length) {
|
||||
this.kycDocumentTypes = this.defaultDocumentTypes.map((d) => ({
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
mandatory: false
|
||||
}));
|
||||
this.kycDocumentTypes = this.filterDocTypesForMinor(
|
||||
this.defaultDocumentTypes.map((d) => ({
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
mandatory: false
|
||||
}))
|
||||
);
|
||||
this.rebuildKycDocPicker();
|
||||
}
|
||||
this.loadExistingKycIfNeeded();
|
||||
@@ -496,6 +529,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (changes['constitutionMeta'] && this.isNonIndividual) {
|
||||
this.updateDocTypesForConstitution();
|
||||
}
|
||||
if (changes['isMinor']) {
|
||||
if (this.isMinor) {
|
||||
this.kycForm.get('hasPan')?.setValue(false);
|
||||
this.kycForm.get('panNumber')?.setValue('');
|
||||
}
|
||||
const filtered = this.filterDocTypesForMinor(this.kycDocumentTypes);
|
||||
if (filtered.length !== this.kycDocumentTypes.length) {
|
||||
this.kycDocumentTypes = filtered;
|
||||
const currentName = this.kycForm.get('kycDocumentName')?.value;
|
||||
if (currentName && !filtered.some((d) => d.name === currentName)) {
|
||||
this.kycForm.get('kycDocumentName')?.setValue('');
|
||||
}
|
||||
this.rebuildKycDocPicker();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Update KYC document types based on selected constitution type */
|
||||
@@ -879,11 +927,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.kycTemplate = template;
|
||||
this.kycTemplateLoaded = true;
|
||||
if (template?.requiredDocuments?.length > 0) {
|
||||
this.kycDocumentTypes = template.requiredDocuments.map((doc: any) => ({
|
||||
name: doc.name,
|
||||
code: doc.code || doc.name,
|
||||
mandatory: doc.mandatory ?? false
|
||||
}));
|
||||
this.kycDocumentTypes = this.filterDocTypesForMinor(
|
||||
template.requiredDocuments.map((doc: any) => ({
|
||||
name: doc.name,
|
||||
code: doc.code || doc.name,
|
||||
mandatory: doc.mandatory ?? false
|
||||
}))
|
||||
);
|
||||
// Update kycDocumentName validator based on mandatory docs
|
||||
const hasMandatory = this.kycDocumentTypes.some((d) => d.mandatory);
|
||||
if (hasMandatory) {
|
||||
@@ -896,11 +946,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
error: () => {
|
||||
// Fallback to defaults if template API unavailable
|
||||
this.kycTemplateLoaded = true;
|
||||
this.kycDocumentTypes = this.defaultDocumentTypes.map((d) => ({
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
mandatory: false
|
||||
}));
|
||||
this.kycDocumentTypes = this.filterDocTypesForMinor(
|
||||
this.defaultDocumentTypes.map((d) => ({
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
mandatory: false
|
||||
}))
|
||||
);
|
||||
this.rebuildKycDocPicker();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user