The CKYC tab's document cards now open a dedicated dialog (with client-side
preview) instead of expanding inline, collecting document type, number, and
issue/expiry dates the same way client creation does. Fixes several bugs
found along the way:
- Uploading a document type that's valid for both identity and address
(Aadhaar, Passport, etc.) no longer 404s or 500s — the "no such endpoint"
and case-sensitive ClientIdentifierStatus.valueOf("Active") crashes are
both resolved.
- Edit Client's Identity-proof and Address-proof steps no longer show each
other's document when both are dual-purpose OVD types, and no longer show
stale/mismatched front-back scans after a document type change.
- Address-proof's own document preview never showed a document at all when
no m_client_kyc record existed yet.
Document number and issue/expiry dates are persisted on the identifier
(documentKey + a shared "Issue date: ..., Expiry date: ..." description
convention) and read back by both the CKYC tab and Edit Client so the same
details appear everywhere.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
820 lines
38 KiB
HTML
820 lines
38 KiB
HTML
<!--
|
|
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>
|
|
|
|
@if (loadingProofs) {
|
|
<div class="addr-proof-loading">
|
|
<fa-icon icon="spinner" [animation]="'spin'"></fa-icon>
|
|
Loading document types…
|
|
</div>
|
|
}
|
|
|
|
<div class="addr-proof-field">
|
|
<div
|
|
class="addr-proof-label-row"
|
|
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px"
|
|
>
|
|
<label class="addr-proof-label" style="margin-bottom: 0"
|
|
><mat-icon>description</mat-icon> Document type</label
|
|
>
|
|
@if (isAddressProofLinkedToKyc && !readOnly) {
|
|
<div
|
|
class="addr-proof-linked-note"
|
|
style="
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #059669;
|
|
font-size: 0.85rem;
|
|
background: #ecfdf5;
|
|
padding: 4px 12px;
|
|
border-radius: 16px;
|
|
"
|
|
>
|
|
<mat-icon style="font-size: 16px; width: 16px; height: 16px">link</mat-icon>
|
|
Automatically linked from KYC step
|
|
<button
|
|
mat-icon-button
|
|
(click)="unlinkAddressProof()"
|
|
matTooltip="Provide a different address proof"
|
|
style="width: 24px; height: 24px; padding: 0; line-height: 24px"
|
|
>
|
|
<mat-icon style="font-size: 16px; color: #047857">edit</mat-icon>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="addr-chip-wrap" [class.addr-chip-wrap--locked]="readOnly || isAddressProofLinkedToKyc">
|
|
@for (group of addressGroups; track group.label; let last = $last) {
|
|
<div class="addr-chip-group-label"><mat-icon>folder_open</mat-icon> {{ group.label }}</div>
|
|
<div class="addr-chip-grid">
|
|
@for (p of group.items; track p.id) {
|
|
<span
|
|
class="addr-chip"
|
|
[class.addr-chip--sel]="isAddrProofSelected(p)"
|
|
[class.addr-chip--locked]="(readOnly || isAddressProofLinkedToKyc) && !isAddrProofSelected(p)"
|
|
[attr.aria-disabled]="readOnly || isAddressProofLinkedToKyc ? 'true' : null"
|
|
(click)="readOnly || isAddressProofLinkedToKyc ? null : selectAddrProof(p)"
|
|
(keydown.enter)="readOnly || isAddressProofLinkedToKyc ? null : selectAddrProof(p)"
|
|
[attr.tabindex]="readOnly || isAddressProofLinkedToKyc ? -1 : 0"
|
|
role="button"
|
|
>
|
|
<mat-icon class="addr-chip__icon">
|
|
{{ isAddrProofSelected(p) ? 'check_circle' : 'radio_button_unchecked' }}
|
|
</mat-icon>
|
|
{{ p.name }}
|
|
</span>
|
|
}
|
|
</div>
|
|
@if (!last) {
|
|
<div class="addr-chip-divider"></div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (selectedAddrProof?.info) {
|
|
<div class="addr-proof-banner">
|
|
<mat-icon>check_circle</mat-icon>
|
|
<span>{{ selectedAddrProof.info }}</span>
|
|
</div>
|
|
}
|
|
|
|
@if (selectedAddrProof) {
|
|
<div class="addr-grid addr-grid--2">
|
|
<mat-form-field appearance="outline" class="addr-ff">
|
|
<mat-label>{{ selectedAddrProof.documentKeyLabel || 'Document number' }}</mat-label>
|
|
<span matPrefix class="addr-prefix"><mat-icon>tag</mat-icon></span>
|
|
<input
|
|
matInput
|
|
formControlName="documentKey"
|
|
[readonly]="readOnly || isAddressProofLinkedToKyc"
|
|
[placeholder]="selectedAddrProof.documentKeyPlaceholder || ''"
|
|
/>
|
|
</mat-form-field>
|
|
@if (selectedAddrProof.requiresDate) {
|
|
<mat-form-field appearance="outline" class="addr-ff">
|
|
<mat-label>Issue / bill date</mat-label>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="addrIssueDatePicker"
|
|
formControlName="issueDate"
|
|
[readonly]="readOnly || isAddressProofLinkedToKyc"
|
|
/>
|
|
<mat-datepicker-toggle
|
|
matSuffix
|
|
[for]="addrIssueDatePicker"
|
|
[disabled]="readOnly || isAddressProofLinkedToKyc"
|
|
></mat-datepicker-toggle>
|
|
<mat-datepicker #addrIssueDatePicker></mat-datepicker>
|
|
</mat-form-field>
|
|
}
|
|
<mat-form-field appearance="outline" class="addr-ff">
|
|
<mat-label>Expiry date (optional)</mat-label>
|
|
<span matPrefix class="addr-prefix"><mat-icon>event_busy</mat-icon></span>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="addrExpiryDatePicker"
|
|
formControlName="expiryDate"
|
|
[readonly]="readOnly || isAddressProofLinkedToKyc"
|
|
/>
|
|
<mat-datepicker-toggle
|
|
matSuffix
|
|
[for]="addrExpiryDatePicker"
|
|
[disabled]="readOnly || isAddressProofLinkedToKyc"
|
|
></mat-datepicker-toggle>
|
|
<mat-datepicker #addrExpiryDatePicker></mat-datepicker>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="addr-upload-grid" [class.addr-upload-grid--two]="selectedAddrProof.sides === 2">
|
|
<div class="addr-upload-slot">
|
|
<div class="addr-upload-head">
|
|
<span class="addr-upload-title">
|
|
<mat-icon>image</mat-icon>
|
|
{{ selectedAddrProof.sides === 2 ? 'Front side' : selectedAddrProof.name + ' (single copy)' }}
|
|
</span>
|
|
<span class="addr-upload-req">Required</span>
|
|
</div>
|
|
<!-- Existing saved document — locked by default. Replace flow
|
|
unlocks automatically when the section's top-level Edit
|
|
button toggles `readOnly` off. -->
|
|
@if (existingAddressProofDocuments.length > 0 && !addrFiles.front && readOnly) {
|
|
<div class="addr-dropzone addr-dropzone--has addr-dropzone--locked">
|
|
<div class="addr-dropzone__existing">
|
|
@if (existingAddrFrontSlotPreview) {
|
|
<img
|
|
[src]="existingAddrFrontSlotPreview"
|
|
alt="Saved address proof"
|
|
class="addr-dropzone__thumb"
|
|
/>
|
|
} @else {
|
|
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
|
}
|
|
<div class="addr-dropzone__existing-meta">
|
|
<span class="addr-dropzone__existing-name">
|
|
{{ existingAddrFrontSlotName }}
|
|
</span>
|
|
<button type="button" class="addr-dropzone__view" (click)="viewExistingAddressProof()">
|
|
<mat-icon>visibility</mat-icon>
|
|
Click here to view
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<label
|
|
class="addr-dropzone"
|
|
[class.addr-dropzone--has]="addrFiles.front || existingAddressProofDocuments.length > 0"
|
|
[class.addr-dropzone--req]="!addrFiles.front && existingAddressProofDocuments.length === 0"
|
|
>
|
|
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onAddrFile($event, 'front')" />
|
|
@if (addrFiles.front) {
|
|
<div class="addr-dropzone__file">
|
|
<mat-icon>insert_drive_file</mat-icon>
|
|
<span class="addr-dropzone__name">{{ addrFiles.front.name }}</span>
|
|
<button
|
|
type="button"
|
|
mat-icon-button
|
|
class="addr-dropzone__remove"
|
|
(click)="$event.preventDefault(); removeAddrFile('front')"
|
|
>
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
} @else if (existingAddressProofDocuments.length > 0) {
|
|
<!-- Edit mode WITH existing doc — let the user view it OR
|
|
click anywhere to replace. -->
|
|
<div class="addr-dropzone__existing">
|
|
@if (existingAddrFrontSlotPreview) {
|
|
<img
|
|
[src]="existingAddrFrontSlotPreview"
|
|
alt="Saved address proof"
|
|
class="addr-dropzone__thumb"
|
|
/>
|
|
} @else {
|
|
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
|
}
|
|
<div class="addr-dropzone__existing-meta">
|
|
<span class="addr-dropzone__existing-name">
|
|
{{ existingAddrFrontSlotName }}
|
|
</span>
|
|
<div class="addr-dropzone__existing-actions">
|
|
<button
|
|
type="button"
|
|
class="addr-dropzone__view"
|
|
(click)="$event.preventDefault(); $event.stopPropagation(); viewExistingAddressProof()"
|
|
>
|
|
<mat-icon>visibility</mat-icon>
|
|
View
|
|
</button>
|
|
<span class="addr-dropzone__sep">·</span>
|
|
<span class="addr-dropzone__existing-hint">
|
|
<mat-icon>cloud_upload</mat-icon>
|
|
Click here to replace
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<div class="addr-dropzone__hint">
|
|
<mat-icon>cloud_upload</mat-icon>
|
|
<span>Click to upload (JPG / PNG / PDF · max 5 MB)</span>
|
|
</div>
|
|
}
|
|
</label>
|
|
}
|
|
</div>
|
|
|
|
@if (selectedAddrProof.sides === 2) {
|
|
<div class="addr-upload-slot">
|
|
<div class="addr-upload-head">
|
|
<span class="addr-upload-title"> <mat-icon>image</mat-icon> Back side </span>
|
|
<!-- "Required" pill is hidden when a saved combined doc covers both sides -->
|
|
@if (!(existingAddressProofDocuments.length > 0 && !addrFiles.back)) {
|
|
<span class="addr-upload-req">Required</span>
|
|
}
|
|
</div>
|
|
<!-- Locked view: saved combined doc covers the back side too. -->
|
|
@if (existingAddressProofDocuments.length > 0 && !addrFiles.back && readOnly) {
|
|
<div class="addr-dropzone addr-dropzone--has addr-dropzone--locked">
|
|
<div class="addr-dropzone__existing">
|
|
@if (existingAddrBackSlotPreview) {
|
|
<img
|
|
[src]="existingAddrBackSlotPreview"
|
|
alt="Saved address proof"
|
|
class="addr-dropzone__thumb"
|
|
/>
|
|
} @else {
|
|
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
|
}
|
|
<div class="addr-dropzone__existing-meta">
|
|
<span class="addr-dropzone__existing-name">
|
|
{{ existingAddrBackSlotName }}
|
|
</span>
|
|
<button type="button" class="addr-dropzone__view" (click)="viewExistingAddressProof()">
|
|
<mat-icon>visibility</mat-icon>
|
|
Click here to view
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<label
|
|
class="addr-dropzone"
|
|
[class.addr-dropzone--has]="addrFiles.back || existingAddressProofDocuments.length > 0"
|
|
[class.addr-dropzone--req]="!addrFiles.back && existingAddressProofDocuments.length === 0"
|
|
>
|
|
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onAddrFile($event, 'back')" />
|
|
@if (addrFiles.back) {
|
|
<div class="addr-dropzone__file">
|
|
<mat-icon>insert_drive_file</mat-icon>
|
|
<span class="addr-dropzone__name">{{ addrFiles.back.name }}</span>
|
|
<button
|
|
type="button"
|
|
mat-icon-button
|
|
class="addr-dropzone__remove"
|
|
(click)="$event.preventDefault(); removeAddrFile('back')"
|
|
>
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
} @else if (existingAddressProofDocuments.length > 0) {
|
|
<!-- Edit mode WITH existing combined doc — same view + replace
|
|
pattern as the front slot, anchored to the saved file. -->
|
|
<div class="addr-dropzone__existing">
|
|
@if (existingAddrBackSlotPreview) {
|
|
<img
|
|
[src]="existingAddrBackSlotPreview"
|
|
alt="Saved address proof"
|
|
class="addr-dropzone__thumb"
|
|
/>
|
|
} @else {
|
|
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
|
}
|
|
<div class="addr-dropzone__existing-meta">
|
|
<span class="addr-dropzone__existing-name">
|
|
{{ existingAddrBackSlotName }}
|
|
</span>
|
|
<div class="addr-dropzone__existing-actions">
|
|
<button
|
|
type="button"
|
|
class="addr-dropzone__view"
|
|
(click)="$event.preventDefault(); $event.stopPropagation(); viewExistingAddressProof()"
|
|
>
|
|
<mat-icon>visibility</mat-icon>
|
|
View
|
|
</button>
|
|
<span class="addr-dropzone__sep">·</span>
|
|
<span class="addr-dropzone__existing-hint">
|
|
<mat-icon>cloud_upload</mat-icon>
|
|
Click here to replace
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<div class="addr-dropzone__hint">
|
|
<mat-icon>cloud_upload</mat-icon>
|
|
<span>Click to upload (JPG / PNG / PDF · max 5 MB)</span>
|
|
</div>
|
|
}
|
|
</label>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Combined preview + re-crop buttons are an edit-mode tool; hide in read-only.
|
|
Also hide when the saved combined PNG is already visible as the thumbnail
|
|
inside both slot tiles (no value in repeating the same image below). -->
|
|
@if (selectedAddrProof.sides === 2 && addrMergedPreview && !readOnly && (addrFiles.front || addrFiles.back)) {
|
|
<div class="addr-merged">
|
|
<div class="addr-merged__head">
|
|
<span class="addr-merged__title">
|
|
<mat-icon>filter_none</mat-icon>
|
|
Combined preview (front + back)
|
|
</span>
|
|
<span class="addr-merged__hint">Saved as a single PNG. Re-crop a side to update the merged file.</span>
|
|
</div>
|
|
<div class="addr-merged__preview">
|
|
<img [src]="addrMergedPreview" alt="Merged address proof" />
|
|
</div>
|
|
<div class="addr-merged__actions">
|
|
<button type="button" mat-stroked-button class="addr-merged__btn" (click)="reCropAddrFile('front')">
|
|
<mat-icon>crop</mat-icon> Re-crop front
|
|
</button>
|
|
<button type="button" mat-stroked-button class="addr-merged__btn" (click)="reCropAddrFile('back')">
|
|
<mat-icon>crop</mat-icon> Re-crop back
|
|
</button>
|
|
</div>
|
|
</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>
|