feat(election,family): family member KYC display, collapsible cards, linked/manual badges, create-as-client flow

- nomination-apply: show full KYC for linked family members via nested forkJoin, accordion expand/collapse cards, owner chips on KYC headers, 'Create as Client' button for manual entries
- family-members-tab: show Linked Client / Manual Entry badges, entry type in detail grid, 'Create as Client' button navigating with prefill state
- create-client + client-general-step: accept prefillData @Input from navigation state, patch form fields from family member data
- Add MPL-2.0 headers to all election module files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-19 17:30:29 +05:30
co-authored by Claude Sonnet 4.6
parent 822fa4c68d
commit 10b301c30c
38 changed files with 7846 additions and 131 deletions
@@ -97,6 +97,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
@Input() clientTemplate: any;
/** Existing client data for edit mode (null for create) */
@Input() clientData: any = null;
/** Pre-fill data from "Create as Client" on a manual family member */
@Input() prefillData: any = null;
/** Read-only mode — disables form editing until user clicks edit icon */
@Input() readOnly = false;
/** Create Client Form */
@@ -415,9 +417,44 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.buildDependencies();
if (this.clientData) {
this.prefillForEdit();
} else if (this.prefillData) {
this.prefillFromFamilyMember();
}
}
/**
* Pre-fills the General step form from a manually-entered family member
* when the user clicks "Create as Client" on the family members tab.
*/
private prefillFromFamilyMember(): void {
const d = this.prefillData;
if (!d) return;
// Parse DOB — Fineract returns [yyyy, mm, dd] array or ISO string
let dob: Date | undefined;
if (d.dateOfBirth) {
if (Array.isArray(d.dateOfBirth)) {
dob = new Date(d.dateOfBirth[0], d.dateOfBirth[1] - 1, d.dateOfBirth[2]);
} else {
const parsed = new Date(d.dateOfBirth);
if (!isNaN(parsed.getTime())) dob = parsed;
}
}
// Patch base fields first
this.createClientForm.patchValue({
mobileNo: d.mobileNo || '',
...(dob ? { dateOfBirth: dob } : {})
});
// Name controls are added dynamically by buildDependencies via legalFormId — set after tick
setTimeout(() => {
if (this.createClientForm.get('firstname') && d.firstname) {
this.createClientForm.get('firstname').setValue(d.firstname);
}
});
}
/**
* Pre-fills the form with existing client data for edit mode.
* Called AFTER buildDependencies so name controls already exist.
@@ -33,11 +33,22 @@
<mat-expansion-panel class="nom-panel">
<mat-expansion-panel-header class="nom-panel__header">
<mat-panel-title class="nom-panel__title">
<mat-icon class="nom-panel__mat-icon">person</mat-icon>
<mat-icon class="nom-panel__mat-icon" [class.nom-panel__mat-icon--linked]="isLinkedClient(member)">
{{ isLinkedClient(member) ? 'person_pin' : 'person' }}
</mat-icon>
<span class="nom-panel__name">{{ displayName(member) }}</span>
@if (getRelationship(member)) {
<span class="nom-panel__badge nom-panel__badge--relation">{{ getRelationship(member) }}</span>
}
@if (isLinkedClient(member)) {
<span class="nom-panel__badge nom-panel__badge--linked">
<mat-icon>person_pin</mat-icon> Linked Client
</span>
} @else {
<span class="nom-panel__badge nom-panel__badge--manual">
<mat-icon>edit_note</mat-icon> Manual Entry
</span>
}
</mat-panel-title>
<mat-panel-description class="nom-panel__desc">
@if (member.dateOfBirth) {
@@ -66,14 +77,42 @@
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
</button>
</div>
@if (!isLinkedClient(member)) {
<button mat-flat-button class="nom-actions__create-client" (click)="createClientFromMember(member)">
<mat-icon>person_add</mat-icon> Create as Client
</button>
}
</div>
}
<div class="nom-grid">
<!-- Entry type row (always shown) -->
<div class="nom-field">
<div class="nom-field__label">Entry Type</div>
<div class="nom-field__value">
@if (isLinkedClient(member)) {
<span class="nom-type-badge nom-type-badge--linked">
<mat-icon>person_pin</mat-icon> Linked Existing Client
</span>
} @else {
<span class="nom-type-badge nom-type-badge--manual">
<mat-icon>edit_note</mat-icon> Manually Entered
</span>
}
</div>
</div>
@if (isLinkedClient(member)) {
<div class="nom-field">
<div class="nom-field__label">CBS Client ID</div>
<div class="nom-field__value nom-field__value--mono">
{{ member.linkedClientCif || member.linkedClientId }}
</div>
</div>
}
@if (member.firstName) {
<div class="nom-field">
<div class="nom-field__label">Name</div>
<div class="nom-field__value">{{ member.firstName }}</div>
<div class="nom-field__value">{{ displayName(member) }}</div>
</div>
}
@if (getRelationship(member)) {
@@ -182,6 +182,41 @@ $orange-light: #fff3e0;
background: $orange-light;
color: $orange;
}
&--linked {
display: inline-flex;
align-items: center;
gap: 3px;
background: $green-light;
color: $green;
border: 1px solid color.adjust($green-light, $lightness: -8%);
mat-icon {
font-size: 12px;
height: 12px;
width: 12px;
}
}
&--manual {
display: inline-flex;
align-items: center;
gap: 3px;
background: $gray-100;
color: $gray-700;
border: 1px solid $gray-200;
mat-icon {
font-size: 12px;
height: 12px;
width: 12px;
}
}
}
&__mat-icon--linked {
background: $green-light;
color: $green;
}
&__desc {
@@ -213,6 +248,29 @@ $orange-light: #fff3e0;
font-weight: 600 !important;
height: 32px !important;
}
&__create-client {
display: inline-flex !important;
align-items: center !important;
gap: 5px !important;
border-radius: 6px !important;
font-size: 12px !important;
font-weight: 700 !important;
height: 32px !important;
background: $green !important;
color: $white !important;
margin-left: auto;
mat-icon {
font-size: 15px;
height: 15px;
width: 15px;
}
&:hover {
background: color.adjust($green, $lightness: -8%) !important;
}
}
}
// Details grid
@@ -269,3 +327,32 @@ $orange-light: #fff3e0;
}
}
}
// Entry-type pill inside expanded grid
.nom-type-badge {
display: inline-flex;
align-items: center;
gap: 4px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
}
&--linked {
background: $green-light;
color: $green;
border: 1px solid color.adjust($green-light, $lightness: -8%);
}
&--manual {
background: $gray-100;
color: $gray-700;
border: 1px solid $gray-200;
}
}
@@ -8,7 +8,7 @@
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
/** Custom Components */
@@ -60,6 +60,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
})
export class FamilyMembersTabComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private clientsService = inject(ClientsService);
private dialog = inject(MatDialog);
private translateService = inject(TranslateService);
@@ -117,7 +118,31 @@ export class FamilyMembersTabComponent {
return '';
}
isLinkedClient(member: any): boolean {
return !!member.linkedClientId;
}
createClientFromMember(member: any): void {
const prefill = {
firstname: (member.firstName || '').toString().trim(),
mobileNo: member.mobileNumber || '',
dateOfBirth: member.dateOfBirth || null,
panNumber: member.panNumber || '',
aadhaarNumber: member.aadhaarNumber || '',
familyMemberId: member.id,
parentClientId: this.clientId
};
this.router.navigate(
[
'/clients',
'create'
],
{ state: { prefill } }
);
}
displayName(member: any): string {
if (member.linkedClientName) return member.linkedClientName;
return member.firstName || '';
}
+4 -4
View File
@@ -115,6 +115,10 @@ export class ClientsService {
return this.http.get(`/clients/${clientId}/accounts`);
}
getClientAddressData(clientId: string) {
return this.http.get(`/client/${clientId}/addresses`);
}
getClientChargesData(clientId: string) {
const httpParams = new HttpParams().set('pendingPayment', 'true');
return this.http.get(`/clients/${clientId}/charges`, { params: httpParams });
@@ -470,10 +474,6 @@ export class ClientsService {
return this.http.get(`/fieldconfiguration/ADDRESS`);
}
getClientAddressData(clientId: string) {
return this.http.get(`/client/${clientId}/addresses`);
}
/**
* Documents attached to an m_client_address row. The generic document API takes
* `client_addresses` as the parent-entity type — see DocumentApiResource.
@@ -40,6 +40,7 @@
}
<mifosx-client-general-step
[clientTemplate]="clientTemplate"
[prefillData]="prefillData"
[readOnly]="isTabReadOnly(0)"
(legalFormChangeEvent)="legalFormChange($event)"
></mifosx-client-general-step>
@@ -85,6 +85,8 @@ export class CreateClientComponent {
selectedTabIndex = 0;
createdClientId = '';
savingClient = false;
/** Pre-fill data passed from "Create as Client" on a manual family member */
prefillData: any = null;
/** Track which tabs are in edit mode (false/undefined = read-only for completed tabs) */
tabEditMode: Record<number, boolean> = {};
@@ -109,6 +111,12 @@ export class CreateClientComponent {
this.clientAddressFieldConfig = data.clientAddressFieldConfig;
this.setDatatables();
});
// Read pre-fill data set by "Create as Client" from a manual family member
const nav = this.router.getCurrentNavigation();
const navState = nav?.extras?.state ?? (typeof history !== 'undefined' ? history.state : null);
if (navState?.prefill) {
this.prefillData = navState.prefill;
}
}
/** Check if a tab is in read-only mode (completed tab navigated back to) */