+112
-28
@@ -1,3 +1,11 @@
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
@@ -10,7 +18,6 @@
|
||||
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="familyMemberForm">
|
||||
|
||||
<!-- ═══ VIEW MODE ═══ -->
|
||||
@if (isViewMode) {
|
||||
<!-- Linked client card -->
|
||||
@@ -18,20 +25,31 @@
|
||||
<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')" />
|
||||
<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>
|
||||
<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')" />
|
||||
<img
|
||||
[src]="linkedSignatureImage"
|
||||
alt="Signature"
|
||||
class="zoomable"
|
||||
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')"
|
||||
/>
|
||||
<span>Signature</span>
|
||||
</div>
|
||||
}
|
||||
@@ -57,7 +75,11 @@
|
||||
</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>
|
||||
<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>
|
||||
@@ -89,19 +111,25 @@
|
||||
<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 }
|
||||
@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>
|
||||
<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>
|
||||
<span class="fm-readonly-value">{{
|
||||
getMarriageAuthorityLabel(familyMemberForm.get('marriageAuthority')?.value)
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -136,7 +164,9 @@
|
||||
</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 class="fm-search-loading">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner> Searching...
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!searching && searchResults.length > 0) {
|
||||
@@ -146,7 +176,18 @@
|
||||
<mat-icon class="fm-search-item__icon">person</mat-icon>
|
||||
<div class="fm-search-item__info">
|
||||
<span class="fm-search-item__name">{{ r.displayName }}</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>
|
||||
<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>
|
||||
}
|
||||
@@ -162,24 +203,37 @@
|
||||
<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')" />
|
||||
<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>
|
||||
<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')" />
|
||||
<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>
|
||||
<button mat-icon-button type="button" (click)="removeLinkedClient()" class="fm-linked-card__remove">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (fetchingLinkedData) {
|
||||
@@ -198,7 +252,11 @@
|
||||
</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>
|
||||
<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>
|
||||
@@ -292,8 +350,13 @@
|
||||
<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)" />
|
||||
<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">
|
||||
@@ -313,15 +376,32 @@
|
||||
<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()" />
|
||||
<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) {
|
||||
<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>
|
||||
@@ -348,9 +428,13 @@
|
||||
<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"
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="familyMemberForm.invalid"
|
||||
[mat-dialog-close]="{ member: familyMember }">
|
||||
[mat-dialog-close]="{ member: familyMember }"
|
||||
>
|
||||
{{ data.context === 'Edit' ? 'Update' : 'Add Member' }}
|
||||
</button>
|
||||
}
|
||||
|
||||
+131
-27
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.fm-dialog__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -9,7 +17,9 @@
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
|
||||
.fm-dialog__icon { color: #1074b9; }
|
||||
.fm-dialog__icon {
|
||||
color: #1074b9;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-mode-toggle {
|
||||
@@ -32,7 +42,11 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
mat-icon { font-size: 18px; width: 18px; height: 18px; }
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #1074b9;
|
||||
@@ -40,11 +54,19 @@
|
||||
color: #1074b9;
|
||||
}
|
||||
|
||||
&:hover:not(.active) { border-color: #94a3b8; }
|
||||
&:hover:not(.active) {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-search-wrapper { position: relative; margin-bottom: 12px; }
|
||||
.fm-search-field { width: 100%; }
|
||||
.fm-search-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fm-search-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fm-search-results {
|
||||
position: absolute;
|
||||
@@ -55,7 +77,7 @@
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -92,13 +114,34 @@
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { background: #f0f7ff; }
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__icon { color: #1074b9; font-size: 20px; }
|
||||
&__info { display: flex; flex-direction: column; }
|
||||
&__name { font-size: 13px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 11px; color: #64748b; }
|
||||
&:hover {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #1074b9;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-linked-card {
|
||||
@@ -111,8 +154,14 @@
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left { flex-shrink: 0; }
|
||||
&__icon { color: #1074b9; }
|
||||
&__left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #1074b9;
|
||||
}
|
||||
|
||||
&__photo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -120,9 +169,25 @@
|
||||
object-fit: cover;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
&__info { flex: 1; display: flex; flex-direction: column; }
|
||||
&__name { font-size: 14px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 12px; color: #64748b; font-family: 'Roboto Mono', monospace; }
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
&__sig {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -139,6 +204,7 @@
|
||||
background: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
@@ -146,7 +212,14 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
&__remove { color: #94a3b8 !important; &:hover { color: #ef4444 !important; } }
|
||||
|
||||
&__remove {
|
||||
color: #94a3b8 !important;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fm-fetching-hint {
|
||||
@@ -201,8 +274,16 @@
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.fm-readonly-label { display: block; margin-bottom: 4px; }
|
||||
.fm-readonly-value { font-size: 13px; color: #475569; line-height: 1.5; }
|
||||
.fm-readonly-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.fm-readonly-value {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-kyc-badge {
|
||||
@@ -225,6 +306,7 @@
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&.not-verified {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
@@ -262,16 +344,22 @@
|
||||
|
||||
.zoomable {
|
||||
cursor: zoom-in;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 18%);
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .image-zoom-panel {
|
||||
.mat-mdc-dialog-container { padding: 0 !important; border-radius: 10px !important; overflow: hidden; }
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0 !important;
|
||||
border-radius: 10px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-file-upload {
|
||||
@@ -290,7 +378,11 @@
|
||||
color: #1074b9;
|
||||
border-color: #cbd5e1;
|
||||
|
||||
mat-icon { font-size: 18px; width: 18px; height: 18px; }
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-file-hint {
|
||||
@@ -308,7 +400,11 @@
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
|
||||
&__icon { color: #1074b9; font-size: 20px; }
|
||||
&__icon {
|
||||
color: #1074b9;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
@@ -318,13 +414,21 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
color: #94a3b8 !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
|
||||
mat-icon { font-size: 16px; width: 16px; height: 16px; }
|
||||
&:hover { color: #ef4444 !important; }
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
-33
@@ -87,7 +87,9 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
/** Whether dialog is in View-only mode */
|
||||
get isViewMode(): boolean { return this.data.context === 'View'; }
|
||||
get isViewMode(): boolean {
|
||||
return this.data.context === 'View';
|
||||
}
|
||||
|
||||
/** Get relationship name from ID */
|
||||
get relationshipName(): string {
|
||||
@@ -109,7 +111,7 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
|
||||
/** Get marriage authority display label */
|
||||
getMarriageAuthorityLabel(value: string): string {
|
||||
return this.marriageAuthorityOptions.find(a => a.value === value)?.label || value || '—';
|
||||
return this.marriageAuthorityOptions.find((a) => a.value === value)?.label || value || '—';
|
||||
}
|
||||
|
||||
/** Marriage certificate file */
|
||||
@@ -150,7 +152,10 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
});
|
||||
if (m.linkedClientId) {
|
||||
this.entryMode = 'linked';
|
||||
this.selectedClient = { id: m.linkedClientId, displayName: m.linkedClientName || m.firstName || `Client #${m.linkedClientId}` };
|
||||
this.selectedClient = {
|
||||
id: m.linkedClientId,
|
||||
displayName: m.linkedClientName || m.firstName || `Client #${m.linkedClientId}`
|
||||
};
|
||||
// Re-fetch linked client details (profile, signature, KYC status)
|
||||
this.fetchLinkedClientData(m.linkedClientId.toString());
|
||||
}
|
||||
@@ -162,13 +167,22 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
|
||||
createFamilyMemberForm() {
|
||||
this.familyMemberForm = this.formBuilder.group({
|
||||
firstName: ['', Validators.required],
|
||||
relationshipId: ['', Validators.required],
|
||||
firstName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
relationshipId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
dateOfBirth: [''],
|
||||
isDependent: [false],
|
||||
remarks: [''],
|
||||
aadhaarNumber: [''],
|
||||
panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)],
|
||||
panNumber: [
|
||||
'',
|
||||
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
|
||||
],
|
||||
nationality: ['INDIAN'],
|
||||
isAlive: [true],
|
||||
marriageDate: [''],
|
||||
@@ -208,7 +222,10 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
this.searching = false;
|
||||
this.clientsService.enrichSearchResults(this.searchResults);
|
||||
},
|
||||
error: () => { this.searchResults = []; this.searching = false; }
|
||||
error: () => {
|
||||
this.searchResults = [];
|
||||
this.searching = false;
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
@@ -242,7 +259,9 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
this.setLinkedFieldsReadonly(true);
|
||||
this.fetchingLinkedData = false;
|
||||
},
|
||||
error: () => { this.fetchingLinkedData = false; }
|
||||
error: () => {
|
||||
this.fetchingLinkedData = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch KYC (PAN, Aadhaar, verification status)
|
||||
@@ -250,8 +269,12 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
next: (kyc: any) => {
|
||||
if (kyc) {
|
||||
const patch: any = {};
|
||||
if (kyc.panNumber) { patch.panNumber = kyc.panNumber; }
|
||||
if (kyc.aadhaarNumberMasked) { patch.aadhaarNumber = kyc.aadhaarNumberMasked; }
|
||||
if (kyc.panNumber) {
|
||||
patch.panNumber = kyc.panNumber;
|
||||
}
|
||||
if (kyc.aadhaarNumberMasked) {
|
||||
patch.aadhaarNumber = kyc.aadhaarNumberMasked;
|
||||
}
|
||||
this.familyMemberForm.patchValue(patch);
|
||||
this.linkedKycStatus = {
|
||||
panVerified: kyc.panVerified || false,
|
||||
@@ -278,8 +301,8 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
// Fetch signature image
|
||||
this.clientsService.getClientDocuments(clientId).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find((d: any) =>
|
||||
d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
const sigDoc = docs?.find(
|
||||
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
);
|
||||
if (sigDoc) {
|
||||
this.clientsService.getClientSignatureImage(clientId, sigDoc.id.toString()).subscribe({
|
||||
@@ -296,8 +319,13 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
|
||||
/** Disable/enable fields that are auto-populated from linked client */
|
||||
private setLinkedFieldsReadonly(readonly: boolean): void {
|
||||
const fields = ['firstName', 'dateOfBirth', 'aadhaarNumber', 'panNumber'];
|
||||
fields.forEach(f => {
|
||||
const fields = [
|
||||
'firstName',
|
||||
'dateOfBirth',
|
||||
'aadhaarNumber',
|
||||
'panNumber'
|
||||
];
|
||||
fields.forEach((f) => {
|
||||
const ctrl = this.familyMemberForm.get(f);
|
||||
if (ctrl) {
|
||||
readonly ? ctrl.disable() : ctrl.enable();
|
||||
@@ -313,7 +341,11 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
this.linkedKycStatus = null;
|
||||
this.setLinkedFieldsReadonly(false);
|
||||
this.familyMemberForm.patchValue({
|
||||
firstName: '', linkedClientId: '', dateOfBirth: '', aadhaarNumber: '', panNumber: ''
|
||||
firstName: '',
|
||||
linkedClientId: '',
|
||||
dateOfBirth: '',
|
||||
aadhaarNumber: '',
|
||||
panNumber: ''
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,26 +417,49 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.img-zoom-dialog { display: flex; flex-direction: column; }
|
||||
.img-zoom-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 16px; background: #f8fafc; border-bottom: 1px solid #e2e8f0;
|
||||
font-size: 14px; font-weight: 600; color: #1e293b;
|
||||
}
|
||||
.img-zoom-body {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 20px; background: #fff; min-height: 200px;
|
||||
}
|
||||
.img-zoom-body img {
|
||||
max-width: 100%; max-height: 70vh; object-fit: contain;
|
||||
border-radius: 6px; box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
`],
|
||||
imports: [MatIcon, STANDALONE_SHARED_IMPORTS]
|
||||
styles: [
|
||||
`
|
||||
.img-zoom-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.img-zoom-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.img-zoom-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
min-height: 200px;
|
||||
}
|
||||
.img-zoom-body img {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`
|
||||
],
|
||||
imports: [
|
||||
MatIcon,
|
||||
STANDALONE_SHARED_IMPORTS
|
||||
]
|
||||
})
|
||||
export class ImageZoomDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<ImageZoomDialogComponent>>(MatDialogRef);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
close() { this.dialogRef.close(); }
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
+36
-7
@@ -1,3 +1,11 @@
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
@@ -75,9 +83,13 @@
|
||||
{{ member.dateOfBirth ? (member.dateOfBirth | dateFormat) : '—' }}
|
||||
</span>
|
||||
<span class="fm-table__col fm-table__col--id fm-mono">
|
||||
@if (member.panNumber) { {{ member.panNumber }} }
|
||||
@else if (member.aadhaarNumber) { {{ member.aadhaarNumber }} }
|
||||
@else { — }
|
||||
@if (member.panNumber) {
|
||||
{{ member.panNumber }}
|
||||
} @else if (member.aadhaarNumber) {
|
||||
{{ member.aadhaarNumber }}
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</span>
|
||||
<span class="fm-table__col fm-table__col--status">
|
||||
@if (member.isAlive !== false) {
|
||||
@@ -87,14 +99,32 @@
|
||||
}
|
||||
</span>
|
||||
<span class="fm-table__col fm-table__col--actions">
|
||||
<button mat-icon-button type="button" (click)="viewFamilyMember(member)" class="fm-action-btn" matTooltip="View">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="viewFamilyMember(member)"
|
||||
class="fm-action-btn"
|
||||
matTooltip="View"
|
||||
>
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
@if (!readOnly) {
|
||||
<button mat-icon-button type="button" (click)="editFamilyMember(member, i)" class="fm-action-btn" matTooltip="Edit">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="editFamilyMember(member, i)"
|
||||
class="fm-action-btn"
|
||||
matTooltip="Edit"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button type="button" (click)="deleteFamilyMember(member.firstName, i)" class="fm-action-btn fm-action-btn--delete" matTooltip="Delete">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="deleteFamilyMember(member.firstName, i)"
|
||||
class="fm-action-btn fm-action-btn--delete"
|
||||
matTooltip="Delete"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -103,5 +133,4 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
+402
-84
@@ -1,11 +1,29 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.zoomable {
|
||||
cursor: zoom-in;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
&:hover { transform: scale(1.08); box-shadow: 0 4px 16px rgba(0,0,0,0.18); }
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 18%);
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .image-zoom-panel {
|
||||
.mat-mdc-dialog-container { padding: 0 !important; border-radius: 10px !important; overflow: hidden; }
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0 !important;
|
||||
border-radius: 10px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.search-no-results-hint {
|
||||
@@ -42,7 +60,7 @@
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +81,10 @@
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.fm-step__icon { color: #1074b9; font-size: 14px; }
|
||||
.fm-step__icon {
|
||||
color: #1074b9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fm-step__count {
|
||||
background: #1074b9;
|
||||
@@ -170,8 +191,13 @@
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { background: #f8fafc; }
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-table__col {
|
||||
@@ -186,11 +212,31 @@
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
&--rel { flex: 0 0 130px; }
|
||||
&--dob { flex: 0 0 120px; color: #64748b; font-size: 12px; }
|
||||
&--id { flex: 0 0 140px; }
|
||||
&--status { flex: 0 0 60px; text-align: center; }
|
||||
&--actions { flex: 0 0 80px; display: flex; gap: 2px; }
|
||||
|
||||
&--rel {
|
||||
flex: 0 0 130px;
|
||||
}
|
||||
|
||||
&--dob {
|
||||
flex: 0 0 120px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--id {
|
||||
flex: 0 0 140px;
|
||||
}
|
||||
|
||||
&--status {
|
||||
flex: 0 0 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--actions {
|
||||
flex: 0 0 80px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-table__avatar {
|
||||
@@ -232,8 +278,15 @@
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
|
||||
&--yes { background: #ecfdf5; color: #059669; }
|
||||
&--no { background: #fef2f2; color: #dc2626; }
|
||||
&--yes {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&--no {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-action-btn {
|
||||
@@ -248,11 +301,16 @@
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&:hover mat-icon { color: #1074b9; }
|
||||
&--delete:hover mat-icon { color: #ef4444; }
|
||||
&:hover mat-icon {
|
||||
color: #1074b9;
|
||||
}
|
||||
|
||||
&--delete:hover mat-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.fm-step__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -260,7 +318,10 @@
|
||||
}
|
||||
|
||||
.fm-table__col {
|
||||
&--type, &--id { display: none; }
|
||||
&--type,
|
||||
&--id {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,11 +345,27 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nom-header__icon { font-size: 16px; color: #1565c0; }
|
||||
.nom-header__title { font-size: 16px; font-weight: 600; color: #1e293b; display: block; }
|
||||
.nom-header__sub { font-size: 11px; color: #94a3b8; display: block; }
|
||||
.nom-header__icon {
|
||||
font-size: 16px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.nom-add-btn { font-size: 13px; }
|
||||
.nom-header__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-header__sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-add-btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Share bar */
|
||||
.nom-share-bar {
|
||||
@@ -302,7 +379,11 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nom-share-bar__label { font-size: 12px; color: #64748b; white-space: nowrap; }
|
||||
.nom-share-bar__label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nom-share-bar__track {
|
||||
flex: 1;
|
||||
@@ -317,8 +398,14 @@
|
||||
background: #1565c0;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
&.complete { background: #059669; }
|
||||
&.over { background: #dc2626; }
|
||||
|
||||
&.complete {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
&.over {
|
||||
background: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-share-bar__value {
|
||||
@@ -327,7 +414,10 @@
|
||||
color: #1565c0;
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
&.over { color: #dc2626; }
|
||||
|
||||
&.over {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nominee card */
|
||||
@@ -346,23 +436,62 @@
|
||||
}
|
||||
|
||||
.nom-card__avatar {
|
||||
width: 36px; height: 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #dbeafe;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1565c0;
|
||||
mat-icon { font-size: 20px; }
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-card__photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nom-card__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nom-card__name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-card__meta {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.nom-card__photo { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||||
.nom-card__info { flex: 1; }
|
||||
.nom-card__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; }
|
||||
.nom-card__meta { font-size: 12px; color: #64748b; display: block; margin-top: 2px; }
|
||||
.nom-card__sig {
|
||||
flex-shrink: 0;
|
||||
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; padding: 2px; }
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
max-width: 60px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-card__actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.nom-card__actions { display: flex; gap: 2px; }
|
||||
|
||||
.nom-card__edit {
|
||||
padding: 12px 16px;
|
||||
@@ -391,8 +520,18 @@
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.nom-card__share-value { font-size: 18px; font-weight: 700; color: #1565c0; display: block; }
|
||||
.nom-card__share-label { font-size: 10px; color: #94a3b8; text-transform: uppercase; }
|
||||
.nom-card__share-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1565c0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-card__share-label {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nom-card__guardian {
|
||||
display: flex;
|
||||
@@ -403,7 +542,13 @@
|
||||
border-top: 1px solid #e0f2fe;
|
||||
font-size: 12px;
|
||||
color: #0c4a6e;
|
||||
mat-icon { font-size: 16px; width: 16px; height: 16px; color: #0ea5e9; }
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty */
|
||||
@@ -420,8 +565,16 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nom-empty__icon { font-size: 36px; width: 36px; height: 36px; color: #cbd5e1; }
|
||||
.nom-empty__sub { font-size: 12px; }
|
||||
.nom-empty__icon {
|
||||
font-size: 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.nom-empty__sub {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Nominee form */
|
||||
.nom-form {
|
||||
@@ -443,33 +596,47 @@
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.nom-search-wrapper { position: relative; }
|
||||
.nom-search-field { width: 100%; }
|
||||
.nom-search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nom-search-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nom-search-prefix {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 8px;
|
||||
mat-icon { font-size: 20px; color: #94a3b8; }
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0; right: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
.nom-search-loading {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
font-size: 13px; color: #64748b;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.nom-search-item {
|
||||
@@ -478,14 +645,43 @@
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
&:hover { background: #f1f5f9; }
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-search-item__icon { color: #94a3b8; font-size: 20px; }
|
||||
.nom-search-item__info { flex: 1; }
|
||||
.nom-search-item__name { font-weight: 500; font-size: 13px; color: #1e293b; display: block; }
|
||||
.nom-search-item__meta { font-size: 11px; color: #94a3b8; display: block; }
|
||||
.nom-search-item__status { font-size: 11px; font-weight: 600; color: #94a3b8; &.active { color: #22c55e; } }
|
||||
.nom-search-item__icon {
|
||||
color: #94a3b8;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nom-search-item__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nom-search-item__name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-search-item__meta {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-search-item__status {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
|
||||
&.active {
|
||||
color: #22c55e;
|
||||
}
|
||||
}
|
||||
|
||||
/* Selected nominee */
|
||||
.nom-selected {
|
||||
@@ -505,30 +701,92 @@
|
||||
}
|
||||
|
||||
.nom-selected__avatar {
|
||||
width: 40px; height: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #dbeafe;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1565c0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nom-selected__photo { width: 100%; height: 100%; object-fit: cover; }
|
||||
.nom-selected__info { flex: 1; }
|
||||
.nom-selected__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; }
|
||||
.nom-selected__meta { font-size: 12px; color: #64748b; display: block; }
|
||||
.nom-selected__photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.nom-selected__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nom-selected__name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-selected__meta {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-selected__sig {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 2px; flex-shrink: 0;
|
||||
img { height: 28px; max-width: 70px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; background: #fff; padding: 2px 4px; }
|
||||
span { font-size: 9px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
height: 28px;
|
||||
max-width: 70px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-selected__remove {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.nom-selected__loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
.nom-selected__remove { color: #94a3b8; }
|
||||
.nom-selected__loading { display: flex; align-items: center; gap: 8px; padding: 12px 16px; font-size: 12px; color: #64748b; }
|
||||
|
||||
.nom-kyc-badge {
|
||||
display: inline-flex; align-items: center; margin-left: 4px;
|
||||
mat-icon { font-size: 14px; width: 14px; height: 14px; }
|
||||
&.verified { color: #16a34a; }
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&.verified {
|
||||
color: #16a34a;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-selected__details {
|
||||
@@ -539,10 +797,29 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nom-detail { display: flex; flex-direction: column; gap: 2px; }
|
||||
.nom-detail__label { font-size: 10px; text-transform: uppercase; color: #94a3b8; letter-spacing: 0.5px; }
|
||||
.nom-detail__value { font-size: 13px; font-weight: 500; color: #1e293b; }
|
||||
.nom-detail__value.mono { font-family: 'Roboto Mono', monospace; font-size: 12px; }
|
||||
.nom-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nom-detail__label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.nom-detail__value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.nom-detail__value.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
.nom-form-fields {
|
||||
@@ -553,8 +830,13 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nom-form-field { width: 100%; }
|
||||
.nom-minor-toggle { padding-top: 8px; }
|
||||
.nom-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nom-minor-toggle {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* Guardian */
|
||||
.nom-guardian-section {
|
||||
@@ -570,7 +852,11 @@
|
||||
font-weight: 600;
|
||||
color: #0c4a6e;
|
||||
margin-bottom: 10px;
|
||||
mat-icon { font-size: 18px; color: #0ea5e9; }
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-guardian-card {
|
||||
@@ -581,13 +867,32 @@
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
mat-icon { color: #0ea5e9; }
|
||||
|
||||
mat-icon {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-guardian-card__info { flex: 1; }
|
||||
.nom-guardian-card__name { font-weight: 600; font-size: 13px; color: #1e293b; display: block; }
|
||||
.nom-guardian-card__meta { font-size: 11px; color: #64748b; display: block; }
|
||||
.nom-guardian-rel { width: 160px; }
|
||||
.nom-guardian-card__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nom-guardian-card__name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-guardian-card__meta {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nom-guardian-rel {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.nom-form-actions {
|
||||
@@ -596,11 +901,24 @@
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
button { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nom-form-fields { grid-template-columns: 1fr; }
|
||||
.nom-guardian-card { flex-wrap: wrap; }
|
||||
.nom-guardian-rel { width: 100%; }
|
||||
@media (width <= 768px) {
|
||||
.nom-form-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.nom-guardian-card {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nom-guardian-rel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
+74
-25
@@ -1,5 +1,9 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
@@ -88,9 +92,20 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
|
||||
/** Relationship options */
|
||||
relationshipOptions = [
|
||||
'Father', 'Mother', 'Spouse', 'Son', 'Daughter',
|
||||
'Brother', 'Sister', 'Uncle', 'Aunt', 'Grandfather',
|
||||
'Grandmother', 'Friend', 'Business Partner', 'Other'
|
||||
'Father',
|
||||
'Mother',
|
||||
'Spouse',
|
||||
'Son',
|
||||
'Daughter',
|
||||
'Brother',
|
||||
'Sister',
|
||||
'Uncle',
|
||||
'Aunt',
|
||||
'Grandfather',
|
||||
'Grandmother',
|
||||
'Friend',
|
||||
'Business Partner',
|
||||
'Other'
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -109,14 +124,18 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
this.loaded = true;
|
||||
this.clientFamilyMembers = Array.isArray(data) ? data : [];
|
||||
},
|
||||
error: () => { this.loaded = true; }
|
||||
error: () => {
|
||||
this.loaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addFamilyMember() {
|
||||
const ref = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||
data: { context: 'Add', options: this.clientTemplate.familyMemberOptions },
|
||||
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
|
||||
width: '640px',
|
||||
maxWidth: '95vw',
|
||||
panelClass: 'fm-dialog-panel'
|
||||
});
|
||||
ref.afterClosed().subscribe((response: any) => {
|
||||
if (response?.member) {
|
||||
@@ -128,14 +147,18 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
viewFamilyMember(member: any) {
|
||||
this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||
data: { context: 'View', member, options: this.clientTemplate.familyMemberOptions },
|
||||
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
|
||||
width: '640px',
|
||||
maxWidth: '95vw',
|
||||
panelClass: 'fm-dialog-panel'
|
||||
});
|
||||
}
|
||||
|
||||
editFamilyMember(member: any, index: number) {
|
||||
const ref = this.dialog.open(ClientFamilyMemberDialogComponent, {
|
||||
data: { context: 'Edit', member, options: this.clientTemplate.familyMemberOptions },
|
||||
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
|
||||
width: '640px',
|
||||
maxWidth: '95vw',
|
||||
panelClass: 'fm-dialog-panel'
|
||||
});
|
||||
ref.afterClosed().subscribe((response: any) => {
|
||||
if (response?.member) {
|
||||
@@ -175,8 +198,7 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
return this.clientFamilyMembers.some((m: any) => {
|
||||
const rel = m.relationship || m.relationshipId;
|
||||
const relName = typeof rel === 'string' ? rel : '';
|
||||
return relName.toLowerCase() === 'spouse' ||
|
||||
m.firstName?.toLowerCase().includes('spouse');
|
||||
return relName.toLowerCase() === 'spouse' || m.firstName?.toLowerCase().includes('spouse');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,14 +213,19 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
const allMembers = Array.isArray(data) ? data : [];
|
||||
this.nominees = allMembers.filter((m: any) => m.nominationType && m.nominationType !== '');
|
||||
},
|
||||
error: () => { this.nomineesLoaded = true; }
|
||||
error: () => {
|
||||
this.nomineesLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNomineeSearch(text: string): void {
|
||||
this.nomineeSearchText = text;
|
||||
clearTimeout(this.nomineeSearchTimer);
|
||||
if (!text || text.length < 2) { this.nomineeSearchResults = []; return; }
|
||||
if (!text || text.length < 2) {
|
||||
this.nomineeSearchResults = [];
|
||||
return;
|
||||
}
|
||||
this.nomineeSearching = true;
|
||||
this.nomineeSearchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 10).subscribe({
|
||||
@@ -207,7 +234,10 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
this.nomineeSearching = false;
|
||||
this.clientsService.enrichSearchResults(this.nomineeSearchResults);
|
||||
},
|
||||
error: () => { this.nomineeSearchResults = []; this.nomineeSearching = false; }
|
||||
error: () => {
|
||||
this.nomineeSearchResults = [];
|
||||
this.nomineeSearching = false;
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
@@ -232,7 +262,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
// Fetch client data for DOB
|
||||
this.clientsService.getClientData(clientId.toString()).subscribe({
|
||||
next: (data: any) => {
|
||||
if (data?.dateOfBirth) { details.dateOfBirth = data.dateOfBirth; }
|
||||
if (data?.dateOfBirth) {
|
||||
details.dateOfBirth = data.dateOfBirth;
|
||||
}
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
@@ -253,7 +285,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
// Fetch profile image
|
||||
this.clientsService.getClientProfileImage(clientId.toString()).subscribe({
|
||||
next: (img: any) => {
|
||||
if (img) { details.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img); }
|
||||
if (img) {
|
||||
details.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img);
|
||||
}
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
@@ -262,8 +296,8 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
// Fetch signature image
|
||||
this.clientsService.getClientDocuments(clientId.toString()).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find((d: any) =>
|
||||
d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
const sigDoc = docs?.find(
|
||||
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
);
|
||||
if (sigDoc) {
|
||||
this.clientsService.getClientSignatureImage(clientId.toString(), sigDoc.id.toString()).subscribe({
|
||||
@@ -273,7 +307,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
} else { done(); }
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
@@ -282,7 +318,10 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
onGuardianSearch(text: string): void {
|
||||
this.guardianSearchText = text;
|
||||
clearTimeout(this.guardianSearchTimer);
|
||||
if (!text || text.length < 2) { this.guardianSearchResults = []; return; }
|
||||
if (!text || text.length < 2) {
|
||||
this.guardianSearchResults = [];
|
||||
return;
|
||||
}
|
||||
this.guardianSearching = true;
|
||||
this.guardianSearchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 10).subscribe({
|
||||
@@ -291,7 +330,10 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
this.guardianSearching = false;
|
||||
this.clientsService.enrichSearchResults(this.guardianSearchResults);
|
||||
},
|
||||
error: () => { this.guardianSearchResults = []; this.guardianSearching = false; }
|
||||
error: () => {
|
||||
this.guardianSearchResults = [];
|
||||
this.guardianSearching = false;
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
@@ -315,7 +357,8 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
saveNominee(): void {
|
||||
if (!this.clientId || !this.selectedNomineeClient || !this.nomineeRelationshipId || !this.nomineePercentageShare) return;
|
||||
if (!this.clientId || !this.selectedNomineeClient || !this.nomineeRelationshipId || !this.nomineePercentageShare)
|
||||
return;
|
||||
if (this.totalSharePercentage > 100) return;
|
||||
|
||||
this.nomineeSaving = true;
|
||||
@@ -360,7 +403,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
});
|
||||
this.resetNomineeForm();
|
||||
},
|
||||
error: () => { this.nomineeSaving = false; }
|
||||
error: () => {
|
||||
this.nomineeSaving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -383,13 +428,13 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
|
||||
/** Max share allowed for the nominee being edited (excludes its own share) */
|
||||
maxShareForEdit(index: number): number {
|
||||
const otherTotal = this.nominees.reduce((sum, n, i) => i === index ? sum : sum + (n.percentageShare || 0), 0);
|
||||
const otherTotal = this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0);
|
||||
return 100 - otherTotal;
|
||||
}
|
||||
|
||||
saveEditNominee(nominee: any, index: number): void {
|
||||
if (!this.editNomineeRelId || !this.editNomineeShare) return;
|
||||
const otherTotal = this.nominees.reduce((sum, n, i) => i === index ? sum : sum + (n.percentageShare || 0), 0);
|
||||
const otherTotal = this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0);
|
||||
if (otherTotal + this.editNomineeShare > 100) return;
|
||||
|
||||
const relOption = this.clientTemplate?.familyMemberOptions?.relationshipIdOptions?.find(
|
||||
@@ -413,8 +458,12 @@ export class ClientFamilyMembersStepComponent implements OnInit {
|
||||
dateFormat: 'dd MMMM yyyy',
|
||||
locale: 'en'
|
||||
};
|
||||
if (nominee.guardianName) { payload.guardianName = nominee.guardianName; }
|
||||
if (nominee.guardianRelationship) { payload.guardianRelationship = nominee.guardianRelationship; }
|
||||
if (nominee.guardianName) {
|
||||
payload.guardianName = nominee.guardianName;
|
||||
}
|
||||
if (nominee.guardianRelationship) {
|
||||
payload.guardianRelationship = nominee.guardianRelationship;
|
||||
}
|
||||
this.clientsService.editFamilyMember(this.clientId, nominee.id, payload).subscribe({ error: () => {} });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user