499 lines
21 KiB
HTML
499 lines
21 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/.
|
|
-->
|
|
|
|
<!--
|
|
Copyright since 2025 Mifos Initiative
|
|
-->
|
|
|
|
<div class="fm-dialog">
|
|
<div class="fm-dialog__header">
|
|
<mat-icon class="fm-dialog__icon">people</mat-icon>
|
|
<span>{{ data.context }} Family Member</span>
|
|
</div>
|
|
|
|
<mat-dialog-content>
|
|
<form [formGroup]="familyMemberForm">
|
|
<!-- ═══ VIEW MODE ═══ -->
|
|
@if (isViewMode) {
|
|
<!-- Linked client card -->
|
|
@if (entryMode === 'linked' && selectedClient) {
|
|
<div class="fm-linked-card">
|
|
<div class="fm-linked-card__left">
|
|
@if (linkedProfileImage) {
|
|
<img
|
|
[src]="linkedProfileImage"
|
|
class="fm-linked-card__photo zoomable"
|
|
alt="Profile"
|
|
(click)="zoomImage(linkedProfileImage, 'Client Photograph')"
|
|
/>
|
|
} @else {
|
|
<mat-icon class="fm-linked-card__icon">person</mat-icon>
|
|
}
|
|
</div>
|
|
<div class="fm-linked-card__info">
|
|
<span class="fm-linked-card__name">{{ selectedClient.displayName }}</span>
|
|
<span class="fm-linked-card__meta"
|
|
>Client ID: {{ selectedClient.id
|
|
}}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}</span
|
|
>
|
|
</div>
|
|
@if (linkedSignatureImage) {
|
|
<div class="fm-linked-card__sig">
|
|
<img
|
|
[src]="linkedSignatureImage"
|
|
alt="Signature"
|
|
class="zoomable"
|
|
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')"
|
|
/>
|
|
<span>Signature</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (fetchingLinkedData) {
|
|
<div class="fm-fetching-hint">
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
<span>Fetching client details...</span>
|
|
</div>
|
|
}
|
|
|
|
<!-- All details in read-only grid -->
|
|
<div class="fm-readonly-grid">
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Full Name</span>
|
|
<span class="fm-readonly-value">{{ familyMemberForm.get('firstName')?.value || '—' }}</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Relationship</span>
|
|
<span class="fm-readonly-value">{{ relationshipName }}</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Date of Birth</span>
|
|
<span class="fm-readonly-value">{{
|
|
familyMemberForm.get('dateOfBirth')?.value
|
|
? (familyMemberForm.get('dateOfBirth')?.value | date: 'dd MMM yyyy')
|
|
: '—'
|
|
}}</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Nationality</span>
|
|
<span class="fm-readonly-value">{{ familyMemberForm.get('nationality')?.value || '—' }}</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Aadhaar</span>
|
|
<span class="fm-readonly-value">
|
|
{{ familyMemberForm.get('aadhaarNumber')?.value || '—' }}
|
|
@if (linkedKycStatus?.aadhaarVerified) {
|
|
<span class="fm-kyc-badge verified"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else if (familyMemberForm.get('aadhaarNumber')?.value) {
|
|
<span class="fm-kyc-badge not-verified"><mat-icon>cancel</mat-icon> Not Verified</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">PAN</span>
|
|
<span class="fm-readonly-value">
|
|
{{ familyMemberForm.get('panNumber')?.value || '—' }}
|
|
@if (linkedKycStatus?.panVerified) {
|
|
<span class="fm-kyc-badge verified"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else if (familyMemberForm.get('panNumber')?.value) {
|
|
<span class="fm-kyc-badge not-verified"><mat-icon>cancel</mat-icon> Not Verified</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Status</span>
|
|
<span class="fm-readonly-value">
|
|
{{ familyMemberForm.get('isAlive')?.value !== false ? 'Alive' : 'Deceased' }}
|
|
@if (familyMemberForm.get('isDependent')?.value) {
|
|
· Dependent
|
|
}
|
|
</span>
|
|
</div>
|
|
@if (isSpouse && familyMemberForm.get('marriageDate')?.value) {
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Marriage Date</span>
|
|
<span class="fm-readonly-value">{{
|
|
familyMemberForm.get('marriageDate')?.value | date: 'dd MMM yyyy'
|
|
}}</span>
|
|
</div>
|
|
}
|
|
@if (isSpouse && familyMemberForm.get('marriageAuthority')?.value) {
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Issuing Authority</span>
|
|
<span class="fm-readonly-value">{{
|
|
getMarriageAuthorityLabel(familyMemberForm.get('marriageAuthority')?.value)
|
|
}}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
@if (familyMemberForm.get('remarks')?.value) {
|
|
<div class="fm-readonly-remarks">
|
|
<span class="fm-readonly-label">Remarks</span>
|
|
<span class="fm-readonly-value">{{ familyMemberForm.get('remarks')?.value }}</span>
|
|
</div>
|
|
}
|
|
@if (isSpouse && familyMemberForm.get('children')?.value?.length > 0) {
|
|
<div class="fm-readonly-remarks">
|
|
<span class="fm-readonly-label">Children</span>
|
|
<div class="fm-children-list">
|
|
@for (child of familyMemberForm.get('children')?.value; track $index) {
|
|
<div class="fm-child-item" style="font-size: 13px; color: #555; margin-top: 4px;">
|
|
<span>{{ child.fullName }}</span>
|
|
@if (child.dateOfBirth) {
|
|
<span> · DOB: {{ child.dateOfBirth | date: 'dd MMM yyyy' }}</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
<!-- ═══ ADD / EDIT MODE ═══ -->
|
|
@if (!isViewMode) {
|
|
<!-- Mode Toggle -->
|
|
<div class="fm-mode-toggle">
|
|
<button type="button" class="fm-mode-btn" [class.active]="entryMode === 'manual'" (click)="setMode('manual')">
|
|
<mat-icon>edit</mat-icon> Enter Manually
|
|
</button>
|
|
<button type="button" class="fm-mode-btn" [class.active]="entryMode === 'linked'" (click)="setMode('linked')">
|
|
<mat-icon>search</mat-icon> Link Existing Client
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Linked Client Search -->
|
|
@if (entryMode === 'linked') {
|
|
@if (!selectedClient) {
|
|
<div class="fm-search-wrapper">
|
|
<mat-form-field appearance="outline" class="fm-search-field">
|
|
<mat-label>Search by Name or Client ID</mat-label>
|
|
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" autocomplete="off" />
|
|
<mat-icon matSuffix>search</mat-icon>
|
|
</mat-form-field>
|
|
@if (searching) {
|
|
<div class="fm-search-results">
|
|
<div class="fm-search-loading">
|
|
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner> Searching...
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (linkedSelectError) {
|
|
<div class="fm-linked-error">
|
|
<mat-icon>error_outline</mat-icon>
|
|
<span>{{ linkedSelectError }}</span>
|
|
</div>
|
|
}
|
|
@if (!searching && searchResults.length > 0) {
|
|
<div class="fm-search-results">
|
|
@for (r of searchResults; track r.id) {
|
|
<div
|
|
class="fm-search-item"
|
|
[class.is-disabled]="isClientAlreadyLinked(r.id)"
|
|
(click)="selectClient(r)"
|
|
>
|
|
<mat-icon class="fm-search-item__icon">person</mat-icon>
|
|
<div class="fm-search-item__info">
|
|
<span class="fm-search-item__name">
|
|
{{ r.displayName }}
|
|
@if (isClientAlreadyLinked(r.id)) {
|
|
<span class="fm-search-item__taken">· already linked</span>
|
|
}
|
|
</span>
|
|
<span class="fm-search-item__meta"
|
|
>ID: {{ r.id }} · {{ r.officeName }}
|
|
@if (r.dateOfBirth) {
|
|
· DOB: {{ r.dateOfBirth | dateFormat }}
|
|
}
|
|
@if (r.fatherSpouseName) {
|
|
· {{ r.fatherSpouseName }}
|
|
}
|
|
@if (r.mobileNo) {
|
|
· {{ r.mobileNo }}
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
|
|
<div class="search-no-results-hint">No clients found</div>
|
|
}
|
|
</div>
|
|
}
|
|
@if (selectedClient) {
|
|
<!-- Linked Client Card with Profile & Signature -->
|
|
<div class="fm-linked-card">
|
|
<div class="fm-linked-card__left">
|
|
@if (linkedProfileImage) {
|
|
<img
|
|
[src]="linkedProfileImage"
|
|
class="fm-linked-card__photo zoomable"
|
|
alt="Profile"
|
|
(click)="zoomImage(linkedProfileImage, 'Client Photograph')"
|
|
/>
|
|
} @else {
|
|
<mat-icon class="fm-linked-card__icon">person</mat-icon>
|
|
}
|
|
</div>
|
|
<div class="fm-linked-card__info">
|
|
<span class="fm-linked-card__name">{{ selectedClient.displayName }}</span>
|
|
<span class="fm-linked-card__meta"
|
|
>Client ID: {{ selectedClient.id
|
|
}}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}</span
|
|
>
|
|
</div>
|
|
@if (linkedSignatureImage) {
|
|
<div class="fm-linked-card__sig">
|
|
<img
|
|
[src]="linkedSignatureImage"
|
|
alt="Signature"
|
|
class="zoomable"
|
|
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')"
|
|
/>
|
|
<span>Signature</span>
|
|
</div>
|
|
}
|
|
<button mat-icon-button type="button" (click)="removeLinkedClient()" class="fm-linked-card__remove">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
|
|
@if (fetchingLinkedData) {
|
|
<div class="fm-fetching-hint">
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
<span>Fetching client details...</span>
|
|
</div>
|
|
}
|
|
|
|
<!-- Read-only fetched details -->
|
|
@if (!fetchingLinkedData) {
|
|
<div class="fm-readonly-grid">
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Full Name</span>
|
|
<span class="fm-readonly-value">{{ familyMemberForm.get('firstName')?.value || '—' }}</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Date of Birth</span>
|
|
<span class="fm-readonly-value">{{
|
|
familyMemberForm.get('dateOfBirth')?.value
|
|
? (familyMemberForm.get('dateOfBirth')?.value | date: 'dd MMM yyyy')
|
|
: '—'
|
|
}}</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">Aadhaar</span>
|
|
<span class="fm-readonly-value">
|
|
{{ familyMemberForm.get('aadhaarNumber')?.value || '—' }}
|
|
@if (linkedKycStatus?.aadhaarVerified) {
|
|
<span class="fm-kyc-badge verified"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else if (familyMemberForm.get('aadhaarNumber')?.value) {
|
|
<span class="fm-kyc-badge not-verified"><mat-icon>cancel</mat-icon> Not Verified</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="fm-readonly-item">
|
|
<span class="fm-readonly-label">PAN</span>
|
|
<span class="fm-readonly-value">
|
|
{{ familyMemberForm.get('panNumber')?.value || '—' }}
|
|
@if (linkedKycStatus?.panVerified) {
|
|
<span class="fm-kyc-badge verified"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else if (familyMemberForm.get('panNumber')?.value) {
|
|
<span class="fm-kyc-badge not-verified"><mat-icon>cancel</mat-icon> Not Verified</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
<!-- Core Fields -->
|
|
<div class="fm-grid">
|
|
@if (entryMode === 'manual') {
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Full Name</mat-label>
|
|
<input matInput required formControlName="firstName" placeholder="Full name of family member" />
|
|
</mat-form-field>
|
|
}
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Relationship</mat-label>
|
|
<mat-select required formControlName="relationshipId">
|
|
@for (rel of data.options.relationshipIdOptions; track rel) {
|
|
<mat-option [value]="rel.id" [disabled]="disabledRelationshipIds.has(rel.id)">
|
|
{{ rel.name }}
|
|
@if (disabledRelationshipIds.has(rel.id)) {
|
|
<span class="fm-rel-taken">· already added</span>
|
|
}
|
|
</mat-option>
|
|
}
|
|
</mat-select>
|
|
<mat-icon
|
|
matSuffix
|
|
class="fm-rel-hint-icon"
|
|
matTooltip="Father, Mother and Spouse can only be added once."
|
|
matTooltipPosition="left"
|
|
>
|
|
info_outline
|
|
</mat-icon>
|
|
</mat-form-field>
|
|
|
|
@if (entryMode === 'manual') {
|
|
<mat-form-field appearance="outline" (click)="dobPicker.open()">
|
|
<mat-label>Date of Birth</mat-label>
|
|
<input matInput [max]="maxDate" [matDatepicker]="dobPicker" formControlName="dateOfBirth" />
|
|
<mat-datepicker-toggle matSuffix [for]="dobPicker"></mat-datepicker-toggle>
|
|
<mat-datepicker #dobPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
}
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Nationality</mat-label>
|
|
<mat-select formControlName="nationality">
|
|
<mat-option value="INDIAN">Indian</mat-option>
|
|
<mat-option value="NRI">NRI</mat-option>
|
|
<mat-option value="FOREIGN_NATIONAL">Foreign National</mat-option>
|
|
<mat-option value="OTHER">Other</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<!-- Marriage Details (Spouse only) -->
|
|
@if (isSpouse) {
|
|
<div class="fm-section-label">Marriage Details</div>
|
|
<div class="fm-grid">
|
|
<mat-form-field appearance="outline" (click)="marriagePicker.open()">
|
|
<mat-label>Marriage Date</mat-label>
|
|
<input matInput [max]="maxDate" [matDatepicker]="marriagePicker" formControlName="marriageDate" />
|
|
<mat-datepicker-toggle matSuffix [for]="marriagePicker"></mat-datepicker-toggle>
|
|
<mat-datepicker #marriagePicker></mat-datepicker>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Issuing Authority</mat-label>
|
|
<mat-select formControlName="marriageAuthority">
|
|
@for (auth of marriageAuthorityOptions; track auth.value) {
|
|
<mat-option [value]="auth.value">{{ auth.label }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<!-- Marriage Certificate Upload -->
|
|
<div class="fm-file-upload">
|
|
@if (!marriageCertFile) {
|
|
<button type="button" mat-stroked-button class="fm-file-btn" (click)="certInput.click()">
|
|
<mat-icon>upload_file</mat-icon> Upload Marriage Certificate
|
|
</button>
|
|
<input
|
|
#certInput
|
|
type="file"
|
|
hidden
|
|
accept=".pdf,.jpg,.jpeg,.png"
|
|
(change)="onMarriageCertSelected($event)"
|
|
/>
|
|
<span class="fm-file-hint">PDF, JPG or PNG (max 5 MB)</span>
|
|
} @else {
|
|
<div class="fm-file-attached">
|
|
<mat-icon class="fm-file-attached__icon">description</mat-icon>
|
|
<span class="fm-file-attached__name">{{ marriageCertFileName }}</span>
|
|
<button mat-icon-button type="button" (click)="removeMarriageCert()" class="fm-file-attached__remove">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- ID Verification (manual mode only) -->
|
|
@if (entryMode === 'manual') {
|
|
<div class="fm-section-label">ID Verification</div>
|
|
<div class="fm-grid">
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Aadhaar Number</mat-label>
|
|
<input
|
|
matInput
|
|
formControlName="aadhaarNumber"
|
|
maxlength="12"
|
|
placeholder="12-digit Aadhaar"
|
|
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>PAN Number</mat-label>
|
|
<input
|
|
matInput
|
|
formControlName="panNumber"
|
|
maxlength="10"
|
|
placeholder="ABCDE1234F"
|
|
style="text-transform: uppercase"
|
|
(input)="
|
|
familyMemberForm
|
|
.get('panNumber')
|
|
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
|
"
|
|
/>
|
|
@if (
|
|
familyMemberForm.get('panNumber')?.hasError('pattern') && familyMemberForm.get('panNumber')?.touched
|
|
) {
|
|
<mat-error>Invalid PAN format</mat-error>
|
|
}
|
|
</mat-form-field>
|
|
</div>
|
|
}
|
|
|
|
<!-- Status -->
|
|
<div class="fm-checkbox-row">
|
|
<mat-checkbox formControlName="isAlive">Is Alive</mat-checkbox>
|
|
<mat-checkbox formControlName="isDependent">Is Dependent</mat-checkbox>
|
|
</div>
|
|
|
|
<!-- Remarks -->
|
|
<mat-form-field appearance="outline" class="fm-full-width">
|
|
<mat-label>Remarks</mat-label>
|
|
<textarea matInput formControlName="remarks" rows="2"></textarea>
|
|
</mat-form-field>
|
|
|
|
<!-- Children Section -->
|
|
@if (isSpouse) {
|
|
<div class="fm-section-label" style="margin-top: 16px;">Children</div>
|
|
<div class="fm-checkbox-row" style="display: flex; align-items: center; justify-content: space-between;">
|
|
<mat-checkbox formControlName="hasChildren">Does this family member have a child/children?</mat-checkbox>
|
|
@if (familyMemberForm.get('hasChildren')?.value) {
|
|
<button mat-stroked-button type="button" color="primary" (click)="openChildrenDialog()" style="margin-left: 16px;">
|
|
<mat-icon>edit</mat-icon> Edit Children Details ({{ children.length }})
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</form>
|
|
</mat-dialog-content>
|
|
|
|
<mat-dialog-actions align="end">
|
|
@if (isViewMode) {
|
|
<button mat-flat-button color="primary" mat-dialog-close type="button">Close</button>
|
|
} @else {
|
|
<button mat-stroked-button mat-dialog-close type="button">Cancel</button>
|
|
<button
|
|
mat-flat-button
|
|
color="primary"
|
|
type="button"
|
|
[disabled]="familyMemberForm.invalid"
|
|
[mat-dialog-close]="{ member: familyMember }"
|
|
>
|
|
{{ data.context === 'Edit' ? 'Update' : 'Add Member' }}
|
|
</button>
|
|
}
|
|
</mat-dialog-actions>
|
|
</div>
|