feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API

- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview
- Hide Financial & Classification section for non-individual entities in personal data and preview
- Fix legal form column in clients list — fetch legalForm from client API for correct display
- Add person/business icon before client name in list rows
- Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API
- Add registration number format hint (mat-hint) based on constitution type
- Add spacing between verification row and constitution type dropdowns
- Reduce table cell padding and prevent action buttons from being clipped
- Add missing MPL-2.0 license headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:10:51 +05:30
co-authored by Claude Opus 4.6
parent ab01111424
commit b890d24bb7
522 changed files with 51991 additions and 21885 deletions
@@ -0,0 +1,454 @@
<!--
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/.
-->
<div class="addr-container">
<div class="addr-header">
<div class="addr-header__left">
<fa-icon icon="map" class="addr-header__icon"></fa-icon>
<div>
<h3 class="addr-header__title">Address Details</h3>
<p class="addr-header__sub">All address fields are India-specific. PIN auto-lookup enabled.</p>
</div>
</div>
</div>
<mat-tab-group class="addr-tabs" [(selectedIndex)]="activeTab" animationDuration="150ms">
<!-- ═══ TAB 1: PERMANENT ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">1</span> Permanent Address
</ng-template>
<div class="addr-panel" [formGroup]="permanentForm">
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 1</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>home</mat-icon></span>
<input matInput required formControlName="addressLine1" placeholder="House / Flat No., Floor, Building Name" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 2</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>signpost</mat-icon></span>
<input matInput formControlName="addressLine2" placeholder="Street, Road, Lane, Colony" />
</mat-form-field>
</div>
<div class="addr-divider"><span>Location</span></div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>pin_drop</mat-icon></span>
<input matInput required formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
(input)="onPinInput('permanent')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
@if (permanentForm.get('pinCode')?.hasError('pattern') && permanentForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@if (pinStatus['permanent'].loading) {
<mat-hint class="pin-loading">Looking up...</mat-hint>
}
@if (pinStatus['permanent'].found) {
<mat-hint class="pin-ok">PIN verified</mat-hint>
}
@if (pinStatus['permanent'].error) {
<mat-error>{{ pinStatus['permanent'].message }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Locality / Area</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>location_on</mat-icon></span>
<mat-select required formControlName="locality">
@for (loc of localityOptions['permanent']; track loc) {
<mat-option [value]="loc">{{ loc }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--3">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>City / Town</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>location_city</mat-icon></span>
<input matInput formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>District</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>map</mat-icon></span>
<input matInput formControlName="district" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>State</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>flag</mat-icon></span>
<input matInput required formControlName="state" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Country</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>public</mat-icon></span>
<input matInput formControlName="country" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Landmark</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>place</mat-icon></span>
<input matInput formControlName="landmark" placeholder="Near / Opp. / Behind..." />
</mat-form-field>
</div>
<div class="addr-divider"><span>Proof of Address</span></div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Proof Type</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>verified_user</mat-icon></span>
<mat-select required formControlName="proofType">
@for (pt of proofTypes; track pt) {
<mat-option [value]="pt">{{ pt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Document Number</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>tag</mat-icon></span>
<input matInput formControlName="proofDocNumber" placeholder="e.g. XXXX XXXX XXXX" />
</mat-form-field>
</div>
</div>
</mat-tab>
<!-- ═══ TAB 2: CURRENT / RESIDENTIAL ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">2</span> Current / Residential
</ng-template>
<div class="addr-panel" [formGroup]="currentForm">
<div class="addr-same-row">
<mat-checkbox [checked]="sameAsPermanent" (change)="toggleSameAsPermanent($event.checked)">
Same as Permanent Address
</mat-checkbox>
</div>
@if (!sameAsPermanent) {
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 1</mat-label>
<input matInput required formControlName="addressLine1" placeholder="House / Flat No., Floor, Building Name" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 2</mat-label>
<input matInput formControlName="addressLine2" placeholder="Street, Road, Lane, Colony" />
</mat-form-field>
</div>
<div class="addr-divider"><span>Location</span></div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<input matInput required formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
(input)="onPinInput('current')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
@if (currentForm.get('pinCode')?.hasError('pattern') && currentForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@if (pinStatus['current'].found) {
<mat-hint class="pin-ok">PIN verified</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Locality / Area</mat-label>
<mat-select required formControlName="locality">
@for (loc of localityOptions['current']; track loc) {
<mat-option [value]="loc">{{ loc }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--3">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>City / Town</mat-label>
<input matInput formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>District</mat-label>
<input matInput formControlName="district" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>State</mat-label>
<input matInput required formControlName="state" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Country</mat-label>
<input matInput formControlName="country" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Landmark</mat-label>
<input matInput formControlName="landmark" placeholder="Near / Opp. / Behind..." />
</mat-form-field>
</div>
<div class="addr-divider"><span>Residency</span></div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Residence Type</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>cottage</mat-icon></span>
<mat-select formControlName="residenceType">
@for (rt of residenceTypes; track rt) {
<mat-option [value]="rt">{{ rt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Years at This Address</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>schedule</mat-icon></span>
<input matInput type="number" formControlName="yearsAtAddress" placeholder="e.g. 3" />
</mat-form-field>
</div>
} @else {
<div class="addr-same-info">
<fa-icon icon="check-circle" class="addr-same-icon"></fa-icon>
Current address is same as Permanent address.
</div>
}
</div>
</mat-tab>
<!-- ═══ TAB 3: OFFICE / WORK ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">3</span> Office / Work
</ng-template>
<div class="addr-panel" [formGroup]="officeForm">
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Organisation / Company Name</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>business</mat-icon></span>
<input matInput formControlName="orgName" placeholder="Legal name of employer / business" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Department / Unit</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>corporate_fare</mat-icon></span>
<input matInput formControlName="department" placeholder="e.g. Finance, Operations" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Office Address Line 1</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>home_work</mat-icon></span>
<input matInput formControlName="addressLine1" placeholder="Office No., Floor, Building Name" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Office Address Line 2</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>signpost</mat-icon></span>
<input matInput formControlName="addressLine2" placeholder="Street, Road, Industrial Area" />
</mat-form-field>
</div>
<div class="addr-divider"><span>Location</span></div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>pin_drop</mat-icon></span>
<input matInput formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
(input)="onPinInput('office')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
@if (officeForm.get('pinCode')?.hasError('pattern') && officeForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@if (pinStatus['office'].found) {
<mat-hint class="pin-ok">PIN verified</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Locality / Area</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>location_on</mat-icon></span>
<mat-select formControlName="locality">
@for (loc of localityOptions['office']; track loc) {
<mat-option [value]="loc">{{ loc }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--3">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>City / Town</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>location_city</mat-icon></span>
<input matInput formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>District</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>map</mat-icon></span>
<input matInput formControlName="district" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>State</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>flag</mat-icon></span>
<input matInput formControlName="state" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Country</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>public</mat-icon></span>
<input matInput formControlName="country" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Office Phone</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>phone</mat-icon></span>
<input matInput formControlName="officePhone" placeholder="+91 XXXXX XXXXX" />
</mat-form-field>
</div>
<div class="addr-divider"><span>Employment Details</span></div>
<div class="addr-grid addr-grid--3">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Employment Type</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>work</mat-icon></span>
<mat-select formControlName="employmentType">
@for (et of employmentTypes; track et) {
<mat-option [value]="et">{{ et }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Employee / Staff ID</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>badge</mat-icon></span>
<input matInput formControlName="employeeId" placeholder="Optional" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Years with Organisation</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>schedule</mat-icon></span>
<input matInput type="number" formControlName="yearsWithOrg" placeholder="e.g. 5" />
</mat-form-field>
</div>
</div>
</mat-tab>
<!-- ═══ TAB 4: CORRESPONDENCE ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">4</span> Correspondence
</ng-template>
<div class="addr-panel" [formGroup]="correspondenceForm">
<div class="addr-copy-row">
<span class="addr-copy-label">Copy address from:</span>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('permanent')">Permanent</button>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('current')">Current</button>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('office')">Office</button>
</div>
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 1</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>home</mat-icon></span>
<input matInput required formControlName="addressLine1" placeholder="House / Flat No., Floor, Building Name" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 2</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>signpost</mat-icon></span>
<input matInput formControlName="addressLine2" placeholder="Street, Road, Lane, Colony" />
</mat-form-field>
</div>
<div class="addr-divider"><span>Location</span></div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>pin_drop</mat-icon></span>
<input matInput required formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
(input)="onPinInput('correspondence')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
@if (correspondenceForm.get('pinCode')?.hasError('pattern') && correspondenceForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@if (pinStatus['correspondence'].found) {
<mat-hint class="pin-ok">PIN verified</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Locality / Area</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>location_on</mat-icon></span>
<mat-select required formControlName="locality">
@for (loc of localityOptions['correspondence']; track loc) {
<mat-option [value]="loc">{{ loc }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--3">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>City / Town</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>location_city</mat-icon></span>
<input matInput formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>District</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>map</mat-icon></span>
<input matInput formControlName="district" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>State</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>flag</mat-icon></span>
<input matInput required formControlName="state" />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Country</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>public</mat-icon></span>
<input matInput formControlName="country" />
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Landmark</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>place</mat-icon></span>
<input matInput formControlName="landmark" placeholder="Near / Opp. / Behind..." />
</mat-form-field>
</div>
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Preferred Communication Mode</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>mail</mat-icon></span>
<mat-select formControlName="commMode">
@for (cm of commModes; track cm) {
<mat-option [value]="cm">{{ cm }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Contact Number for Correspondence</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>phone_android</mat-icon></span>
<input matInput formControlName="contactNumber" placeholder="+91 XXXXX XXXXX" />
</mat-form-field>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
@@ -0,0 +1,201 @@
/**
* Copyright since 2025 Mifos Initiative
*/
$blue: #1353a0;
$navy: #0f172a;
$border: #e2e8f0;
$muted: #64748b;
.addr-container {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
}
.addr-header {
background: $navy;
padding: 16px 24px;
&__left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon {
font-size: 18px;
color: #60a5fa;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin: 0;
}
&__sub {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
margin: 2px 0 0;
}
}
// Tabs
.addr-tabs {
::ng-deep .mat-mdc-tab-labels {
background: #f8fafc;
border-bottom: 1px solid $border;
}
}
.addr-tab-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 10px;
font-weight: 600;
margin-right: 6px;
background: #f1f5f9;
color: $muted;
}
// Panel
.addr-panel {
padding: 24px;
}
// Grids
.addr-grid {
display: grid;
gap: 4px 16px;
margin-bottom: 4px;
&--1 { grid-template-columns: 1fr; }
&--2 { grid-template-columns: 1fr 1fr; }
&--3 { grid-template-columns: 1fr 1fr 1fr; }
}
.addr-ff {
width: 100%;
}
// Divider
.addr-divider {
display: flex;
align-items: center;
gap: 10px;
margin: 4px 0 14px;
span {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: $muted;
white-space: nowrap;
}
&::before, &::after {
content: '';
flex: 1;
height: 1px;
background: $border;
}
}
// PIN status hints
.pin-loading {
color: $muted !important;
}
.pin-ok {
color: #059669 !important;
font-weight: 600;
}
// Same as permanent
.addr-same-row {
padding: 10px 14px;
background: #f8fafc;
border: 1px solid $border;
border-radius: 6px;
margin-bottom: 18px;
}
.addr-same-info {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 18px;
background: #ecfdf5;
border: 1px solid #a7f3d0;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
color: #065f46;
}
.addr-same-icon {
font-size: 16px;
color: #059669;
}
// Copy row
.addr-copy-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid $border;
border-radius: 6px;
margin-bottom: 18px;
}
.addr-copy-label {
font-size: 12px;
font-weight: 500;
color: $muted;
}
.addr-copy-btn {
font-size: 12px !important;
padding: 0 12px !important;
height: 30px !important;
border-radius: 4px !important;
}
// Responsive
@media (max-width: 768px) {
.addr-grid--2, .addr-grid--3 {
grid-template-columns: 1fr;
}
.addr-panel {
padding: 16px;
}
}
.addr-prefix {
display: inline-flex;
align-items: center;
padding-left: 4px;
padding-right: 8px;
margin-right: 8px;
border-right: 1.5px solid #cbd5e1;
line-height: 1;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
color: #1565c0;
}
}
@@ -0,0 +1,319 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input, OnInit, OnChanges, SimpleChanges, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIcon } from '@angular/material/icon';
import { ClientsService } from '../../clients.service';
@Component({
selector: 'mifosx-client-india-address-step',
templateUrl: './client-india-address-step.component.html',
styleUrls: ['./client-india-address-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatCheckbox,
MatTabsModule,
MatIcon
]
})
export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
private fb = inject(UntypedFormBuilder);
private http = inject(HttpClient);
private clientsService = inject(ClientsService);
/** Client ID — when set, loads existing addresses for edit mode */
@Input() clientId: string = '';
/** Read-only mode — disables form editing until user clicks edit icon */
@Input() readOnly = false;
activeTab = 0;
sameAsPermanent = false;
permanentForm: UntypedFormGroup = this.fb.group({
addressLine1: ['', Validators.required],
addressLine2: [''],
pinCode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],
locality: ['', Validators.required],
city: [''],
district: [''],
state: ['', Validators.required],
country: [{ value: 'India', disabled: true }],
landmark: [''],
proofType: ['', Validators.required],
proofDocNumber: ['']
});
currentForm: UntypedFormGroup = this.fb.group({
addressLine1: ['', Validators.required],
addressLine2: [''],
pinCode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],
locality: ['', Validators.required],
city: [''],
district: [''],
state: ['', Validators.required],
country: [{ value: 'India', disabled: true }],
landmark: [''],
residenceType: [''],
yearsAtAddress: ['']
});
officeForm: UntypedFormGroup = this.fb.group({
orgName: [''],
department: [''],
addressLine1: [''],
addressLine2: [''],
pinCode: ['', Validators.pattern(/^\d{6}$/)],
locality: [''],
city: [''],
district: [''],
state: [''],
country: [{ value: 'India', disabled: true }],
officePhone: [''],
employmentType: [''],
employeeId: [''],
yearsWithOrg: ['']
});
correspondenceForm: UntypedFormGroup = this.fb.group({
addressLine1: ['', Validators.required],
addressLine2: [''],
pinCode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],
locality: ['', Validators.required],
city: [''],
district: [''],
state: ['', Validators.required],
country: [{ value: 'India', disabled: true }],
landmark: [''],
commMode: [''],
contactNumber: ['']
});
// PIN lookup state per tab
pinStatus: Record<string, { loading: boolean; found: boolean; error: boolean; message: string }> = {
permanent: { loading: false, found: false, error: false, message: '' },
current: { loading: false, found: false, error: false, message: '' },
office: { loading: false, found: false, error: false, message: '' },
correspondence: { loading: false, found: false, error: false, message: '' }
};
localityOptions: Record<string, string[]> = {
permanent: [],
current: [],
office: [],
correspondence: []
};
proofTypes = [
'Aadhaar Card', 'Passport', 'Voter ID', 'Driving Licence',
'Utility Bill (not older than 3 months)', 'Bank Statement'
];
residenceTypes = ['Owned', 'Rented', 'Leased', 'Company Provided', 'Parental / Family Owned'];
employmentTypes = [
'Salaried (Private)', 'Salaried (Government / PSU)',
'Self-Employed Professional', 'Business Owner', 'Retired'
];
commModes = ['Post / Courier', 'Email', 'SMS', 'WhatsApp'];
private pinTimers: Record<string, any> = {};
private pinCache: Record<string, any[]> = {};
private addressLoaded = false;
/** Address type name → tab mapping for loading existing data */
private typeToTab: Record<string, string> = {};
ngOnInit(): void {
if (this.clientId) {
this.loadExistingAddresses();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['readOnly']) {
const forms = [this.permanentForm, this.currentForm, this.officeForm, this.correspondenceForm];
for (const form of forms) {
if (this.readOnly) {
form.disable({ emitEvent: false });
} else {
form.enable({ emitEvent: false });
}
}
}
}
/**
* Load existing addresses from Fineract API and populate forms.
* Maps Fineract fields back to India address fields.
*/
private loadExistingAddresses(): void {
if (this.addressLoaded) return;
this.clientsService.getClientAddressData(this.clientId).subscribe({
next: (addresses: any[]) => {
this.addressLoaded = true;
if (!addresses?.length) return;
// Build type-to-tab mapping from address type names
const tabMap: Record<string, string> = {
permanent: 'permanent', 'permanent address': 'permanent',
residential: 'current', current: 'current', 'current / residential': 'current', communication: 'current',
office: 'office', 'office / work': 'office', business: 'office', work: 'office',
correspondence: 'correspondence', mailing: 'correspondence', postal: 'correspondence'
};
for (const addr of addresses) {
const typeName = (addr.addressType || '').toLowerCase().trim();
const tab = tabMap[typeName] || (addresses.indexOf(addr) === 0 ? 'permanent' : null);
if (!tab) continue;
const form = this.getForm(tab);
form.patchValue({
addressLine1: addr.addressLine1 || '',
addressLine2: addr.addressLine2 || '',
pinCode: addr.postalCode || '',
locality: addr.townVillage || '',
city: addr.city || '',
district: addr.countyDistrict || '',
state: addr.stateName || '',
landmark: addr.street || ''
});
}
},
error: () => { this.addressLoaded = true; }
});
}
onPinInput(tab: string) {
const form = this.getForm(tab);
const pin = form.get('pinCode')?.value?.replace(/\D/g, '') || '';
form.get('pinCode')?.setValue(pin, { emitEvent: false });
clearTimeout(this.pinTimers[tab]);
if (pin.length < 6) {
this.pinStatus[tab] = { loading: false, found: false, error: false, message: '' };
this.clearPinFields(tab);
return;
}
if (this.pinCache[pin]) {
this.applyPinData(tab, this.pinCache[pin]);
return;
}
this.pinStatus[tab] = { loading: true, found: false, error: false, message: 'Looking up...' };
this.pinTimers[tab] = setTimeout(() => {
this.http.get<any[]>(`https://api.postalpincode.in/pincode/${pin}`).subscribe({
next: (data: any) => {
if (data[0]?.Status === 'Success') {
this.pinCache[pin] = data[0].PostOffice;
this.applyPinData(tab, data[0].PostOffice);
this.pinStatus[tab] = { loading: false, found: true, error: false, message: 'Found' };
} else {
this.clearPinFields(tab);
this.pinStatus[tab] = { loading: false, found: false, error: true, message: 'Invalid PIN' };
}
},
error: () => {
this.pinStatus[tab] = { loading: false, found: false, error: true, message: 'Network error' };
}
});
}, 550);
}
private applyPinData(tab: string, offices: any[]) {
const seen = new Set<string>();
this.localityOptions[tab] = offices.filter(o => {
if (seen.has(o.Name)) return false;
seen.add(o.Name);
return true;
}).map(o => o.Name);
const form = this.getForm(tab);
const f = offices[0];
form.patchValue({
locality: this.localityOptions[tab][0] || '',
city: f.Block || f.Division || f.District || '',
district: f.District || '',
state: f.State || ''
});
}
private clearPinFields(tab: string) {
this.localityOptions[tab] = [];
const form = this.getForm(tab);
form.patchValue({ locality: '', city: '', district: '', state: '' });
}
getForm(tab: string): UntypedFormGroup {
switch (tab) {
case 'permanent': return this.permanentForm;
case 'current': return this.currentForm;
case 'office': return this.officeForm;
case 'correspondence': return this.correspondenceForm;
default: return this.permanentForm;
}
}
toggleSameAsPermanent(checked: boolean) {
this.sameAsPermanent = checked;
if (checked) {
const pv = this.permanentForm.getRawValue();
this.currentForm.patchValue({
addressLine1: pv.addressLine1,
addressLine2: pv.addressLine2,
pinCode: pv.pinCode,
locality: pv.locality,
city: pv.city,
district: pv.district,
state: pv.state,
landmark: pv.landmark
});
this.localityOptions['current'] = [...this.localityOptions['permanent']];
this.pinStatus['current'] = { ...this.pinStatus['permanent'] };
}
}
copyFrom(source: string) {
const srcForm = this.getForm(source);
const sv = srcForm.getRawValue();
this.correspondenceForm.patchValue({
addressLine1: sv.addressLine1,
addressLine2: sv.addressLine2,
pinCode: sv.pinCode,
locality: sv.locality,
city: sv.city,
district: sv.district,
state: sv.state,
landmark: sv.landmark || ''
});
this.localityOptions['correspondence'] = [...(this.localityOptions[source] || [])];
this.pinStatus['correspondence'] = { ...this.pinStatus[source] };
}
/** Returns all address data for submission */
get addressData(): any {
return {
permanent: this.permanentForm.getRawValue(),
current: this.sameAsPermanent ? { ...this.permanentForm.getRawValue(), sameAsPermanent: true } : this.currentForm.getRawValue(),
office: this.officeForm.getRawValue(),
correspondence: this.correspondenceForm.getRawValue()
};
}
get hasAddressData(): boolean {
return this.permanentForm.get('addressLine1')?.value?.length > 0;
}
}