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:
co-authored by
Claude Opus 4.6
parent
ab01111424
commit
b890d24bb7
+454
@@ -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>
|
||||
+201
@@ -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;
|
||||
}
|
||||
}
|
||||
+319
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user