minor change on client

This commit is contained in:
2026-07-28 10:45:31 +05:30
parent a9c60083bf
commit 349dc91951
23 changed files with 305 additions and 101 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

@@ -466,6 +466,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.
@@ -521,7 +566,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 || '',
@@ -599,6 +648,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);
});
}
}
/**
@@ -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 */
@@ -874,11 +922,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) {
@@ -891,11 +941,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();
}
});
+1 -1
View File
@@ -191,7 +191,7 @@ const routes: Routes = [
{
path: 'form60',
component: Form60TabComponent,
data: { title: 'Form 60', breadcrumb: 'Form 60', routeParamBreadcrumb: false }
data: { title: 'Form 97', breadcrumb: 'Form 97', routeParamBreadcrumb: false }
},
{
path: 'documents',
@@ -19,14 +19,14 @@
<div class="f60-pan-ok__icon-wrap">
<mat-icon class="f60-pan-ok__icon">verified</mat-icon>
</div>
<h3 class="f60-pan-ok__title">Form 60 Not Required</h3>
<h3 class="f60-pan-ok__title">Form 97 Not Required</h3>
<p class="f60-pan-ok__desc">
PAN <strong>{{ panNumber }}</strong> is attached to this client.
@if (panVerified) {
<mat-icon class="f60-pan-ok__tick">check_circle</mat-icon> <span class="f60-pan-ok__verified">Verified</span>
}
</p>
<p class="f60-pan-ok__rule">As per Income Tax Rule 114B, Form 60 is only required when PAN is not available.</p>
<p class="f60-pan-ok__rule">As per Income Tax Rule 114B, Form 97 is only required when PAN is not available.</p>
</div>
}
@@ -36,7 +36,7 @@
<div class="f60-warning">
<mat-icon class="f60-warning__icon">warning_amber</mat-icon>
<div class="f60-warning__text">
<strong>PAN not provided</strong> — Form 60 declaration is mandatory as per Income Tax Rule 114B.
<strong>PAN not provided</strong> — Form 97 declaration is mandatory as per Income Tax Rule 114B.
</div>
</div>
@@ -45,7 +45,7 @@
<div class="f60-card">
<div class="f60-card__header">
<mat-icon class="f60-card__header-icon">description</mat-icon>
<h3>Form 60 Declaration</h3>
<h3>Form 97 Declaration</h3>
<span class="f60-card__status">Submitted</span>
<button mat-stroked-button color="primary" (click)="toggleEditMode()" class="f60-card__edit">
<mat-icon>edit</mat-icon> Edit
@@ -123,7 +123,7 @@
<div class="f60-card">
<div class="f60-card__header">
<mat-icon class="f60-card__header-icon">edit_note</mat-icon>
<h3>Form 60 Declaration</h3>
<h3>Form 97 Declaration</h3>
</div>
<div class="f60-card__body">
<form [formGroup]="form60Form" (ngSubmit)="submit()">
@@ -204,7 +204,7 @@
<button type="button" mat-stroked-button (click)="toggleEditMode()">Cancel</button>
}
<button type="submit" mat-flat-button color="primary" [disabled]="!form60Form.valid">
<mat-icon>check</mat-icon> Submit Form 60
<mat-icon>check</mat-icon> Submit Form 97
</button>
</div>
</form>
@@ -224,14 +224,14 @@ export class Form60TabComponent implements OnInit, OnDestroy {
this.isEditMode = false;
this.alertService.alert({
type: 'success',
message: 'Form 60 submitted successfully.'
message: 'Form 97 submitted successfully.'
});
this.loadForm60Data();
},
error: (err: any) => {
this.alertService.alert({
type: 'error',
message: err?.error?.developerMessage || err?.error?.defaultUserMessage || 'Failed to submit Form 60.'
message: err?.error?.developerMessage || err?.error?.defaultUserMessage || 'Failed to submit Form 97.'
});
}
});
@@ -71,6 +71,7 @@
<mifosx-client-kyc-step
#clientKyc
[isNonIndividual]="legalFormType === 2"
[isMinor]="clientGeneralStep?.isMinor || false"
[clientId]="createdClientId"
[constitutionMeta]="clientGeneralStep?.selectedConstitution"
[entityName]="clientGeneralStep?.createClientForm?.get('fullname')?.value || ''"
@@ -146,6 +147,7 @@
}
<mifosx-client-india-address-step
#clientIndiaAddress
[isMinor]="clientGeneralStep?.isMinor || false"
[readOnly]="isTabReadOnly(addressTabIdx)"
></mifosx-client-india-address-step>
</div>
@@ -111,6 +111,12 @@ export class CreateClientComponent {
this.route.data.subscribe((data: { clientTemplate: any; clientAddressFieldConfig: any }) => {
this.clientTemplate = data.clientTemplate;
this.clientAddressFieldConfig = data.clientAddressFieldConfig;
// This app's RouteReusableStrategy keeps this SAME component instance alive across
// repeat visits to /clients/create, so ngOnInit/constructor won't re-run for a second
// "create new client" session — reset transient state here instead, since `route.data`
// re-emits on every navigation regardless of whether the component was reused. Must run
// BEFORE setDatatables(), which reads legalFormType to filter clientTemplate.datatables.
this.resetForNewClient();
this.setDatatables();
});
// Read pre-fill data set by "Create as Client" from a manual family member
@@ -121,6 +127,20 @@ export class CreateClientComponent {
}
}
/** Resets session state that could otherwise leak between clients when the route is reused. */
private resetForNewClient(): void {
this.legalFormType = 1;
this.selectedTabIndex = 0;
this.createdClientId = '';
this.savingClient = false;
this.saveProgress = 0;
this.tabEditMode = {};
this.highestTabReached = 0;
this.validationError = '';
this.prefillData = null;
this.clientGeneralStep?.resetForNewClient();
}
/** Check if a tab is in read-only mode (completed tab navigated back to) */
isTabReadOnly(tabIndex: number): boolean {
// Only tabs that have been passed (below highest reached) AND not explicitly set to edit mode
@@ -149,6 +149,7 @@
<mifosx-client-kyc-step
#clientKyc
[isNonIndividual]="legalFormType === 2"
[isMinor]="clientGeneralStep?.isMinor || false"
[clientId]="clientId"
[readOnly]="isTabReadOnly(1)"
></mifosx-client-kyc-step>
@@ -206,6 +207,7 @@
<mifosx-client-india-address-step
#clientIndiaAddress
[clientId]="clientId"
[isMinor]="clientGeneralStep?.isMinor || false"
[readOnly]="isTabReadOnly(addressTabIdx)"
></mifosx-client-india-address-step>
</div>
+13
View File
@@ -70,6 +70,19 @@ export const PROOF_META: Record<string, Partial<ProofType>> = {
documentKeyPlaceholder: 'NPR reference number',
info: 'NPR letter — upload full document.'
},
BIRTH_CERTIFICATE: {
sides: 1,
documentKeyLabel: 'Birth certificate registration number',
documentKeyPlaceholder: 'Registration / certificate number',
category: 'Government-issued ID',
info: 'Birth certificate issued by the Municipal Corporation / Registrar of Births & Deaths — accepted proof of identity for minor clients as per RBI KYC norms.'
},
SCHOOL_ID_CARD: {
sides: 1,
documentKeyLabel: 'School ID / admission number',
documentKeyPlaceholder: 'Student ID number',
info: 'School identity card bearing photograph — accepted proof of identity for minor clients as per RBI KYC norms.'
},
UTILITY_BILL: {
sides: 1,
category: 'Utility bills',
+3 -3
View File
@@ -1468,13 +1468,13 @@
Balance Certificate
</button>
<button mat-menu-item routerLink="/letter-mgmt/form-60" tabindex="0">
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 60
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 97
</button>
<button mat-menu-item tabindex="0">
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 61
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 98
</button>
<button mat-menu-item routerLink="/letter-mgmt/form-15gh" tabindex="0">
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 15G and H
<fa-icon icon="file-signature" class="menu-item-icon" style="color: #15803d"></fa-icon> Form 121
</button>
<button mat-menu-item routerLink="/letter-mgmt/hypothecation-certificate" tabindex="0">
<fa-icon icon="file-alt" class="menu-item-icon" style="color: #15803d"></fa-icon>
@@ -969,7 +969,7 @@ export function buildForm60Pdf(r: Form60Record): jsPDF {
let y = paintLetterhead(
c,
'Form 60 Declaration',
'Form 97 Declaration',
'CERTIFICATE REFERENCE',
`F60-${r.customerId}`,
'PAN Status',
@@ -85,7 +85,7 @@
} @else if (isBalance && balance) {
<span class="cpd-stage-pill">{{ balance.asOnDate | date: 'd MMM y' }}</span>
} @else if (isForm60 && form60) {
<span class="cpd-stage-pill cpd-stage-pill--amber">No PAN — Form 60</span>
<span class="cpd-stage-pill cpd-stage-pill--amber">No PAN — Form 97</span>
} @else if (isForm15gh && form15gh) {
<span class="cpd-stage-pill cpd-stage-pill--teal">Form {{ form15gh.kind }}</span>
} @else if (isHypothecation && hypothecation) {
@@ -313,7 +313,7 @@
</p>
}
<!-- ── FORM 60 ── -->
<!-- ── FORM 97 ── -->
@if (isForm60 && form60) {
<p class="cpd-para">
Declaration under <strong>Section 114B</strong> of the Income-tax Rules, 1962, to be made by a person who does
@@ -367,7 +367,7 @@
<tr class="cpd-summary-total">
<td colspan="3">PAN STATUS</td>
<td>
<strong>NOT ALLOTTED — Form 60 declared</strong>
<strong>NOT ALLOTTED — Form 97 declared</strong>
</td>
</tr>
</tbody>
@@ -151,9 +151,9 @@ export class CertificatePreviewDialogComponent {
case 'balance':
return 'Balance Certificate';
case 'form60':
return 'Form 60 Declaration';
return 'Form 97 Declaration';
case 'form15gh':
return this.form15gh?.kind === '15H' ? 'Form 15H Declaration' : 'Form 15G Declaration';
return 'Form 121 Declaration';
case 'hypothecation':
return 'Hypothecation Certificate';
case 'noc':
@@ -11,11 +11,8 @@
<div class="cert-head-l">
<fa-icon icon="file-signature" class="cert-head-ic"></fa-icon>
<div>
<h2 class="cert-head-t">Form 15G &amp; 15H</h2>
<div class="cert-head-s">
TDS-waiver declarations for FD / RD interest income —
<strong>15G</strong> for individuals below 60 years, <strong>15H</strong> for senior citizens (60+).
</div>
<h2 class="cert-head-t">Form 121</h2>
<div class="cert-head-s">TDS-waiver declarations for FD / RD interest income — Form 121.</div>
</div>
</div>
</div>
@@ -26,14 +23,14 @@
<fa-icon icon="user" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ count15G() | number }}</div>
<div class="cert-kpi-l">Form 15G (under 60)</div>
<div class="cert-kpi-l">Form 121 (under 60)</div>
</div>
</div>
<div class="cert-kpi cert-kpi--amber">
<fa-icon icon="user-tie" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ count15H() | number }}</div>
<div class="cert-kpi-l">Form 15H (60+)</div>
<div class="cert-kpi-l">Form 121 (60+)</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
@@ -63,11 +60,11 @@
</div>
<div class="cert-filter">
<label>Form</label>
<label>Form Type</label>
<select [(ngModel)]="kindFilter">
<option value="">Both forms</option>
<option value="15G">15G — Under 60</option>
<option value="15H">15H — Senior citizens (60+)</option>
<option value="">All Form 121</option>
<option value="15G">Form 121 (Below 60 years)</option>
<option value="15H">Form 121 (Senior Citizen 60+)</option>
</select>
</div>
@@ -92,7 +89,7 @@
<thead>
<tr>
<th>Customer · PAN</th>
<th>Form</th>
<th>Form Type</th>
<th>Age</th>
<th>Branch</th>
<th class="num">Deposit Principal</th>
@@ -115,7 +112,7 @@
[class.cert-pill--blue]="formKind(r) === '15G'"
[class.cert-pill--amber]="formKind(r) === '15H'"
>
Form {{ formKind(r) }}
Form 121 ({{ formKind(r) }})
</span>
</td>
<td class="small">{{ age(r.dob) }} yrs</td>
@@ -146,9 +146,9 @@ export class Form15ghComponent implements OnInit {
const kind = this.formKind(r);
this.dscSign.promptThenDownload(
{
title: `Form ${kind} Declaration`,
title: `Form 121 (${kind}) Declaration`,
reference: `${r.customerId} · ${r.customerName} · ${r.fy}`,
filename: `Form-${kind}-${r.customerId}-${r.fy.replace(/\s+/g, '')}.pdf`
filename: `Form121-${r.customerId}-${r.fy.replace(/\s+/g, '')}.pdf`
},
(dsc) => downloadForm15ghPdf(r, kind, dsc)
);
@@ -11,7 +11,7 @@
<div class="cert-head-l">
<fa-icon icon="file-signature" class="cert-head-ic"></fa-icon>
<div>
<h2 class="cert-head-t">Form 60 Declaration</h2>
<h2 class="cert-head-t">Form 97 Declaration</h2>
<div class="cert-head-s">
Income Tax Rule 114B — required for any customer transacting above the threshold who does
<strong>not</strong> have a PAN on file.
@@ -33,7 +33,7 @@
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ totalFiled() | number }}</div>
<div class="cert-kpi-l">Form 60 Filed</div>
<div class="cert-kpi-l">Form 97 Filed</div>
</div>
</div>
<div class="cert-kpi cert-kpi--amber">
@@ -58,7 +58,7 @@
<div class="cert-filter">
<label>PAN status</label>
<select [(ngModel)]="panFilter">
<option value="MISSING">Without PAN (Form 60 applicable)</option>
<option value="MISSING">Without PAN (Form 97 applicable)</option>
<option value="ON_FILE">With PAN on file</option>
<option value="">All customers</option>
</select>
@@ -91,7 +91,7 @@
<th>Trigger Transaction</th>
<th class="num">Amount</th>
<th>PAN Status</th>
<th>Form 60</th>
<th>Form 97</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
@@ -146,7 +146,7 @@
class="cert-icon-btn primary"
[disabled]="r.hasPan"
(click)="viewCertificate(r)"
matTooltip="View / Print Form 60"
matTooltip="View / Print Form 97"
>
<fa-icon icon="eye"></fa-icon>
</button>
@@ -121,7 +121,7 @@ export class Form60Component implements OnInit {
viewCertificate(r: Form60Record): void {
if (r.hasPan) {
this.snack.open(`${r.customerName} already has a PAN on file — Form 60 is not applicable.`, 'OK', {
this.snack.open(`${r.customerName} already has a PAN on file — Form 97 is not applicable.`, 'OK', {
duration: 3200
});
return;
@@ -139,14 +139,14 @@ export class Form60Component implements OnInit {
downloadCertificate(r: Form60Record): void {
if (r.hasPan) {
this.snack.open(`${r.customerName} already has a PAN on file — Form 60 is not applicable.`, 'OK', {
this.snack.open(`${r.customerName} already has a PAN on file — Form 97 is not applicable.`, 'OK', {
duration: 3200
});
return;
}
this.dscSign.promptThenDownload(
{
title: 'Form 60 Declaration',
title: 'Form 97 Declaration',
reference: `${r.customerId} · ${r.customerName}`,
filename: `Form60-${r.customerId}.pdf`
},
@@ -44,12 +44,12 @@ const routes: Routes = [
{
path: 'letter-mgmt/form-60',
component: Form60Component,
data: { title: 'Form 60', breadcrumb: 'Form 60' }
data: { title: 'Form 97', breadcrumb: 'Form 97' }
},
{
path: 'letter-mgmt/form-15gh',
component: Form15ghComponent,
data: { title: 'Form 15G & 15H', breadcrumb: 'Form 15G/H' }
data: { title: 'Form 121', breadcrumb: 'Form 121' }
},
{
path: 'letter-mgmt/hypothecation-certificate',
+3 -3
View File
@@ -2084,8 +2084,8 @@
"Floating Rates": "Floating Rates",
"Floating rate": "Floating Rate",
"Follow": "Follow",
"Form 60": "Form 60",
"Form 60 Declaration": "Form 60 Declaration",
"Form 60": "Form 97",
"Form 60 Declaration": "Form 97 Declaration",
"Frequency": "Frequency",
"Frequency Interval": "Frequency Interval",
"Frequency Interval for compounding": "Frequency Interval for compounding",
@@ -3983,7 +3983,7 @@
"Organization available languages": "Organization can select a language here from the available languages here.",
"Original Schedule": "Original Schedule",
"Overdue Charges": "Overdue Charges",
"PAN not provided. Please fill Form 60 as per Income Tax Rule 114B.": "PAN not provided. Please fill Form 60 as per Income Tax Rule 114B.",
"PAN not provided. Please fill Form 60 as per Income Tax Rule 114B.": "PAN not provided. Please fill Form 97 as per Income Tax Rule 114B.",
"Password Preferences": "Password Preferences",
"Passwords": "Passwords",
"Pay Charge": "Pay Charge",
+2 -3
View File
@@ -9,10 +9,9 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260723',
'hash': 'ca85bae4'
'version': '260728',
'hash': 'a9c60083'
},
'allow_switching_backend_instance': true
};
/* tslint:enable */