feat: update savings/current/rd account UI flows

This commit is contained in:
Nitesh Yadav
2026-04-28 13:16:07 +05:30
parent f917467b68
commit 31b2490f09
22 changed files with 6150 additions and 179 deletions
@@ -42,9 +42,23 @@ export class AddRelatedClientDialogComponent implements OnDestroy {
Validators.min(1),
Validators.max(100)
]
]
],
signingAuthority: [null],
role: [null]
});
readonly signingAuthorityOptions = [
{ value: 'SINGLE', label: 'Single' },
{ value: 'JOINTLY', label: 'Jointly' },
{ value: 'EITHER_OR_SURVIVOR', label: 'Either or Survivor' }
];
readonly roleOptions = [
{ value: 'PRIMARY', label: 'Primary' },
{ value: 'JOINT', label: 'Joint' },
{ value: 'NOMINEE', label: 'Nominee' }
];
searchResults: any[] = [];
selectedClient: any = null;
isSearching = false;
@@ -89,8 +103,10 @@ export class AddRelatedClientDialogComponent implements OnDestroy {
this.dialogRef.close({
clientId: this.selectedClient.id,
clientName: this.selectedClient.displayName,
clientCif: this.selectedClient.cifNumber || this.selectedClient.accountNumber || '',
sharePercentage: this.form.get('sharePercentage').value
clientCif: this.selectedClient.cifNumber || this.selectedClient.cifNo || '',
sharePercentage: this.form.get('sharePercentage').value,
signingAuthority: this.form.get('signingAuthority').value || null,
role: this.form.get('role').value || null
});
}
@@ -59,6 +59,22 @@
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field" (click)="establishmentDatePicker.open()">
<mat-label>Establishment Date</mat-label>
<input matInput [matDatepicker]="establishmentDatePicker" formControlName="establishmentDate" />
<mat-datepicker-toggle matSuffix [for]="establishmentDatePicker"></mat-datepicker-toggle>
<mat-datepicker #establishmentDatePicker></mat-datepicker>
<mat-hint>Date of entity / business establishment</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field" (click)="startOperationDatePicker.open()">
<mat-label>Start of Operation Date</mat-label>
<input matInput [matDatepicker]="startOperationDatePicker" formControlName="startOperationDate" />
<mat-datepicker-toggle matSuffix [for]="startOperationDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startOperationDatePicker></mat-datepicker>
<mat-hint>Date when operations commenced</mat-hint>
</mat-form-field>
}
</div>
</div>
@@ -139,6 +155,536 @@
}
}
<!-- ══ Status of Account (RBI) — Individual Clients Only ════════ -->
@if (currentAccountProductSelected) {
<div class="soa-card">
<div class="soa-card__header">
<div class="soa-card__header-left">
<div class="soa-card__icon">
<i class="fas fa-id-card"></i>
</div>
<div>
<h3 class="soa-card__title">Status of Account</h3>
<span class="soa-card__sub">As per RBI guidelines — select account holder type</span>
</div>
</div>
<span class="soa-pill">RBI Compliant</span>
</div>
<!-- Auto-detected client info bar -->
@if (clientDob) {
<div class="soa-client-info-bar">
<div class="soa-client-info-bar__item">
<mat-icon>cake</mat-icon>
<span>Date of Birth: <strong>{{ formatTemplateDate(clientDob) }}</strong></span>
</div>
<div class="soa-client-info-bar__item">
<mat-icon>person_pin</mat-icon>
<span>Age: <strong>{{ clientAge }} years</strong></span>
</div>
@if (clientAge !== null && clientAge < 18) {
<span class="soa-auto-pill soa-auto-pill--minor">
<mat-icon>child_care</mat-icon> Auto-detected: Minor
</span>
} @else if (clientAge !== null && clientAge >= 60) {
<span class="soa-auto-pill soa-auto-pill--senior">
<mat-icon>elderly</mat-icon> Auto-detected: Senior Citizen
</span>
} @else {
<span class="soa-auto-pill soa-auto-pill--ok"><mat-icon>check_circle</mat-icon> Age Verified</span>
}
</div>
}
<!-- Status selector tiles -->
<div class="soa-tiles">
@for (opt of accountStatusOptions; track opt.value) {
<button
type="button"
class="soa-tile"
[class.soa-tile--selected]="selectedAccountStatus === opt.value"
[class.soa-tile--disabled]="isTileDisabled(opt.value)"
[disabled]="isTileDisabled(opt.value)"
(click)="!isTileDisabled(opt.value) && (selectedAccountStatus = opt.value)"
[title]="getTileTooltip(opt.value)"
>
<mat-icon class="soa-tile__icon" [style.color]="isTileDisabled(opt.value) ? '#cbd5e1' : opt.color">
{{ opt.icon }}
</mat-icon>
<span class="soa-tile__label">{{ opt.label }}</span>
@if (isTileDisabled(opt.value)) {
<span class="soa-tile__lock"><mat-icon>lock</mat-icon></span>
}
@if (selectedAccountStatus === opt.value) {
<mat-icon class="soa-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<!-- ── JOINT ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'JOINT') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #8b5cf6">A</span>
<div>
<span class="soa-block__title">Joint Account Holders</span>
<span class="soa-block__hint">Add all co-applicants linked to this joint current account</span>
</div>
</div>
<button mat-flat-button color="primary" type="button" class="soa-add-btn" (click)="openAddJointHolderDialog()">
<mat-icon>person_add</mat-icon> Add Holder
</button>
</div>
<div class="soa-table-wrap">
<table class="soa-tbl">
<thead>
<tr>
<th class="soa-th soa-th--sr">Sr.</th>
<th class="soa-th">Holder Name</th>
<th class="soa-th">CIF No.</th>
<th class="soa-th soa-th--center">Ownership %</th>
<th class="soa-th soa-th--center">Signing Authority</th>
<th class="soa-th soa-th--center">Role</th>
<th class="soa-th soa-th--center">Action</th>
</tr>
</thead>
<tbody>
<!-- Primary account holder row (non-removable) -->
<tr class="soa-tr soa-tr--primary">
<td class="soa-td soa-td--sr">1</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar soa-avatar--primary">
{{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }}
</div>
<div>
<span class="soa-client-name">{{ primaryClientName || 'Primary Holder' }}</span>
<span class="soa-primary-tag">Primary Account Holder</span>
</div>
</div>
</td>
<td class="soa-td"><span class="soa-mono">{{ primaryClientCif || '—' }}</span></td>
<td class="soa-td soa-td--center">
<input
class="soa-pct-input"
type="number"
min="0"
max="100"
[(ngModel)]="primaryHolderShare"
[ngModelOptions]="{ standalone: true }"
placeholder="0"
/>
<span class="soa-pct-unit">%</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-sign-toggle soa-sign-toggle--mandatory">
<mat-icon>lock</mat-icon> Mandatory
</span>
</td>
<td class="soa-td soa-td--center">Primary Account Holder</td>
<td class="soa-td soa-td--center"><span class="soa-no-action">—</span></td>
</tr>
<!-- Co-holders -->
@for (h of jointHolders; track h; let i = $index) {
<tr class="soa-tr" [class.soa-tr--mandatory]="h.isMandatory">
<td class="soa-td soa-td--sr">{{ i + 2 }}</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar" [class.soa-avatar--mandatory]="h.isMandatory">
{{ h.clientName[0].toUpperCase() }}
</div>
<div>
<span class="soa-client-name">{{ h.clientName }}</span>
@if (h.isMandatory) {
<span class="soa-mandatory-tag">Mandatory Signatory</span>
}
</div>
</div>
</td>
<td class="soa-td"><span class="soa-mono">{{ h.clientCif || '—' }}</span></td>
<td class="soa-td soa-td--center">
<span class="soa-pct-badge" [class.soa-pct-badge--full]="h.sharePercentage === 100">
{{ h.sharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<button
type="button"
class="soa-sign-toggle"
[class.soa-sign-toggle--mandatory]="h.isMandatory"
[class.soa-sign-toggle--optional]="!h.isMandatory"
(click)="toggleJointHolderMandatory(h)"
>
<mat-icon>{{ h.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
</button>
</td>
<td class="soa-td soa-td--center">Co-Holder</td>
<td class="soa-td soa-td--center">
<button mat-icon-button color="warn" type="button" (click)="removeJointHolder(i)">
<mat-icon>delete_outline</mat-icon>
</button>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="soa-tr-total">
<td colspan="3" class="soa-td soa-td--total-label">Total Ownership</td>
<td class="soa-td soa-td--center">
<span
class="soa-pct-badge"
[class.soa-pct-badge--ok]="totalJointSharePercentage === 100"
[class.soa-pct-badge--warn]="totalJointSharePercentage !== 100"
>
{{ totalJointSharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-mandatory-count">
<mat-icon>lock</mat-icon> {{ mandatoryJointCount + 1 }} Mandatory
</span>
</td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty" style="margin-top: 8px">
<mat-icon>group_add</mat-icon>
<span>Click <strong>Add Holder</strong> to add joint account holders.</span>
</div>
}
@if (totalJointSharePercentage !== 100) {
<div class="soa-alert soa-alert--warn" style="margin-top: 8px">
<mat-icon>warning_amber</mat-icon>
Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%.
</div>
} @else {
<div class="soa-alert soa-alert--ok" style="margin-top: 8px">
<mat-icon>check_circle</mat-icon>
Ownership allocation is complete (100%).
</div>
}
</div>
<!-- Mode of Operation -->
<div class="soa-block soa-block--sep">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #7c3aed">B</span>
<div>
<span class="soa-block__title">Mode of Operation</span>
<span class="soa-block__hint">Auto-set from mandatory holders — override manually if required</span>
</div>
</div>
@if (modeOfOperation) {
<span class="soa-auto-badge"><mat-icon>auto_awesome</mat-icon> Auto</span>
}
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty">
<mat-icon>how_to_vote</mat-icon>
<span>Add joint holders in Section A first.</span>
</div>
} @else {
<div class="soa-mode-grid">
@for (opt of modeOfOperationOptions; track opt.value) {
<button
type="button"
class="soa-mode-card"
[class.soa-mode-card--selected]="modeOfOperation === opt.value"
(click)="modeOfOperation = opt.value"
>
<mat-icon class="soa-mode-card__icon">{{ opt.icon }}</mat-icon>
<div class="soa-mode-card__code">{{ opt.label }}</div>
<div class="soa-mode-card__desc">{{ opt.sublabel }}</div>
@if (modeOfOperation === opt.value) {
<mat-icon class="soa-mode-card__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-mode-summary" [class.soa-mode-summary--set]="modeOfOperation">
@if (modeOfOperation) {
<mat-icon>how_to_vote</mat-icon>
<span>Mode of Operation: <strong>{{ selectedModeLabel }}</strong></span>
<span class="soa-mode-summary__from">
({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional)
</span>
} @else {
<mat-icon>info</mat-icon>
<span>Set Mandatory/Optional for holders above to auto-select mode.</span>
}
</div>
}
</div>
}
<!-- ── MINOR ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'MINOR') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #f59e0b">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">child_care</mat-icon>
</span>
<div>
<span class="soa-block__title">Minor Account Details</span>
<span class="soa-block__hint">As per RBI — guardian must be an existing registered client</span>
</div>
</div>
<span class="soa-pill soa-pill--minor">Guardian Required</span>
</div>
<div class="soa-minor-grid">
<mat-form-field appearance="outline" class="soa-field" (click)="minorDobPicker.open()">
<mat-label>Minor's Date of Birth</mat-label>
<input
matInput
[matDatepicker]="minorDobPicker"
[max]="maxDate"
[(ngModel)]="minorDob"
[ngModelOptions]="{ standalone: true }"
/>
<mat-datepicker-toggle matSuffix [for]="minorDobPicker"></mat-datepicker-toggle>
<mat-datepicker #minorDobPicker></mat-datepicker>
@if (clientDob) {
<mat-hint>Auto-filled from client profile</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Must be below 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Majority (Auto)</mat-label>
<input matInput [value]="majorityDate" readonly />
<mat-hint>Auto-calculated: DOB + 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Guardian Relationship</mat-label>
<mat-select [(ngModel)]="guardianRelationship" [ngModelOptions]="{ standalone: true }">
@for (rel of guardianRelationshipOptions; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="soa-guardian-search">
<div class="soa-guardian-search__label">
<mat-icon>manage_accounts</mat-icon>
<span>Search &amp; Link Guardian (existing client)</span>
</div>
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
@if (guardianName) {
<div class="soa-guardian-selected">
<mat-icon class="soa-guardian-selected__icon">verified_user</mat-icon>
<div>
<div class="soa-guardian-selected__name">{{ guardianName }}</div>
<div class="soa-guardian-selected__cif">CIF: {{ guardianCif || 'N/A' }}</div>
</div>
<span class="soa-pill soa-pill--ok">Guardian Linked</span>
</div>
}
</div>
<div class="soa-info-banner soa-info-banner--minor">
<mat-icon>info</mat-icon>
<ul>
<li>Minor below 10 years — Guardian must operate the account.</li>
<li>Minor above 10 years — Can operate independently (bank discretion).</li>
<li>Account must be converted to regular savings on attaining majority.</li>
<li>Fresh KYC of the minor must be done on majority.</li>
</ul>
</div>
</div>
}
<!-- ── SENIOR CITIZEN ────────────────────────────────────────── -->
@if (selectedAccountStatus === 'SENIOR_CITIZEN') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #10b981">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">elderly</mat-icon>
</span>
<div>
<span class="soa-block__title">Senior Citizen Account</span>
<span class="soa-block__hint">Age ≥ 60 years — eligible for preferential banking services</span>
</div>
</div>
<span class="soa-pill soa-pill--senior">Age ≥ 60</span>
</div>
<div class="soa-minor-grid" style="margin-bottom: 16px">
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Birth</mat-label>
<input matInput [value]="formatTemplateDate(clientDob)" readonly />
<mat-hint>From client profile</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Age ≥ 60 required for Senior Citizen</mat-hint>
</mat-form-field>
</div>
<div class="soa-sc-benefits">
<div class="soa-sc-benefit">
<mat-icon>priority_high</mat-icon>
<div>
<strong>Priority Service</strong>
<span>Dedicated counters / assisted banking at branches</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>percent</mat-icon>
<div>
<strong>Preferential Interest Rate</strong>
<span>Additional 0.25%–0.50% on deposits (as per product)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>health_and_safety</mat-icon>
<div>
<strong>Doorstep Banking</strong>
<span>Eligible for home banking services (RBI circular)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>gavel</mat-icon>
<div>
<strong>Nomination Mandatory</strong>
<span>RBI mandate — ensure nomination is registered</span>
</div>
</div>
</div>
<div class="soa-info-banner soa-info-banner--senior">
<mat-icon>info</mat-icon>
<ul>
<li>Age proof (DOB document) mandatory at time of account opening.</li>
<li>Nomination registration is compulsory (Form DA-1).</li>
<li>Bank must offer dedicated helpline and priority grievance redressal.</li>
<li>Door-step banking services must be offered (RBI Master Direction 2023).</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL MALE ───────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_MALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #3b82f6">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Male)</span>
<span class="soa-block__hint">Select special category if applicable — governs applicable schemes &amp; benefits</span>
</div>
</div>
<span class="soa-pill" style="background: #dbeafe; color: #1d4ed8; border-color: #bfdbfe">Special Category</span>
</div>
<div class="soa-category-grid">
@for (opt of specialCategoryOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value"
[class.soa-cat-tile--male]="true"
(click)="selectedSpecialCategory = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedSpecialCategory === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--male">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).</li>
<li>NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL FEMALE ──────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #ec4899">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person_outline</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Female)</span>
<span class="soa-block__hint">Select weaker section category if applicable — governs PMJDY &amp; priority schemes</span>
</div>
</div>
<span class="soa-pill" style="background: #fce7f3; color: #be185d; border-color: #f9a8d4">Weaker Section</span>
</div>
<div class="soa-category-grid">
@for (opt of weakerSectionOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedWeakerSection === opt.value"
[class.soa-cat-tile--female]="true"
(click)="selectedWeakerSection = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedWeakerSection === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--female">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).</li>
<li>PMJDY beneficiary — ₹10,000 overdraft facility &amp; accident insurance cover applicable.</li>
<li>SHG members — group linkage lending &amp; special deposit rates applicable.</li>
</ul>
</div>
</div>
}
</div>
}
<!-- ══ Non-Individual Account Details (Indian Banking Style) ═════ -->
@if (isNonIndividualClient && currentAccountProductSelected) {
<div class="ni-card">
@@ -712,6 +712,941 @@ $ni-bg: #f9fafb;
}
}
// ── Status of Account (RBI) ───────────────────────────────────────────────
.soa-card {
margin-top: 16px;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #fff;
overflow: visible;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
border-radius: 10px 10px 0 0;
}
&__header-left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: #ede9fe;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 16px;
color: #7c3aed;
}
}
&__title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
&__sub {
font-size: 11px;
color: #6b7280;
display: block;
}
}
.soa-pill {
display: inline-flex;
align-items: center;
padding: 3px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #ede9fe;
color: #7c3aed;
border: 1px solid #ddd6fe;
&--minor {
background: #fef3c7;
color: #b45309;
border-color: #fde68a;
}
&--senior {
background: #d1fae5;
color: #065f46;
border-color: #a7f3d0;
}
&--ok {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
.soa-client-info-bar {
display: flex;
align-items: center;
gap: 20px;
padding: 10px 24px;
background: #f0f9ff;
border-bottom: 1px solid #bae6fd;
flex-wrap: wrap;
&__item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #0369a1;
mat-icon {
font-size: 16px;
height: 16px;
width: 16px;
}
strong {
font-weight: 700;
}
}
}
.soa-auto-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
margin-left: auto;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
}
&--minor {
background: #fef3c7;
color: #b45309;
border: 1px solid #fde68a;
}
&--senior {
background: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
&--ok {
background: #dcfce7;
color: #15803d;
border: 1px solid #bbf7d0;
}
}
.soa-tiles {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
padding: 16px 24px;
border-bottom: 1px solid #f1f5f9;
@media (width <= 900px) {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) {
grid-template-columns: repeat(2, 1fr);
}
}
.soa-tile {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 10px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition:
border-color 0.15s,
box-shadow 0.15s;
position: relative;
&:hover {
border-color: #a5b4fc;
box-shadow: 0 2px 8px rgb(99 102 241 / 12%);
}
&--selected {
border-color: #6366f1;
background: #eef2ff;
box-shadow: 0 2px 12px rgb(99 102 241 / 20%);
}
&__icon {
font-size: 28px;
height: 28px;
width: 28px;
}
&__label {
font-size: 12px;
font-weight: 600;
color: #374151;
text-align: center;
}
&__check {
position: absolute;
top: 6px;
right: 6px;
font-size: 16px;
height: 16px;
width: 16px;
color: #6366f1;
}
&__lock {
position: absolute;
top: 4px;
right: 4px;
display: flex;
align-items: center;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
color: #94a3b8;
}
}
&--disabled {
opacity: 0.45;
cursor: not-allowed;
background: #f8fafc;
border-color: #e2e8f0;
box-shadow: none;
&:hover {
border-color: #e2e8f0;
box-shadow: none;
}
}
}
.soa-block {
padding: 16px 24px;
&--sep {
border-top: 1px dashed #e2e8f0;
}
&__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px;
}
&__head-left {
display: flex;
align-items: flex-start;
gap: 12px;
}
&__letter {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
margin-top: 2px;
}
&__title {
font-size: 13px;
font-weight: 600;
color: #1f2937;
display: block;
}
&__hint {
font-size: 11px;
color: #6b7280;
display: block;
}
}
.soa-table-wrap {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.soa-tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.soa-th {
padding: 10px 14px;
background: #f8fafc;
text-align: left;
font-weight: 600;
font-size: 11px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1px solid #e2e8f0;
&--sr {
width: 44px;
}
&--center {
text-align: center;
}
}
.soa-td {
padding: 10px 14px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
&--sr {
text-align: center;
font-size: 12px;
color: #94a3b8;
font-weight: 600;
}
&--center {
text-align: center;
}
&--total-label {
font-size: 12px;
font-weight: 600;
color: #475569;
text-align: right;
}
}
.soa-tr {
transition: background 0.1s;
&:hover {
background: #f8fafc;
}
&--mandatory {
background: #fdf4ff;
}
&--primary {
background: #f0f9ff;
border-left: 3px solid #0891b2;
}
}
.soa-avatar--primary {
background: #cffafe;
color: #0e7490;
}
.soa-primary-tag {
font-size: 10px;
color: #0e7490;
font-weight: 600;
background: #cffafe;
padding: 1px 7px;
border-radius: 999px;
display: inline-block;
}
.soa-pct-input {
width: 56px;
padding: 4px 6px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
text-align: center;
color: #1f2937;
&:focus {
outline: none;
border-color: #0891b2;
box-shadow: 0 0 0 2px rgb(8 145 178 / 15%);
}
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
opacity: 1;
}
}
.soa-pct-unit {
font-size: 13px;
font-weight: 600;
color: #64748b;
margin-left: 2px;
}
.soa-no-action {
color: #cbd5e1;
font-size: 16px;
}
.soa-tr-total {
background: #f8fafc;
}
.soa-client-cell {
display: flex;
align-items: center;
gap: 10px;
}
.soa-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #dbeafe;
color: #1d4ed8;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--mandatory {
background: #ede9fe;
color: #7c3aed;
}
}
.soa-client-name {
font-weight: 600;
font-size: 13px;
color: #1f2937;
display: block;
}
.soa-mandatory-tag {
font-size: 10px;
color: #7c3aed;
font-weight: 600;
background: #ede9fe;
padding: 1px 7px;
border-radius: 999px;
}
.soa-mono {
font-family: 'Roboto Mono', monospace;
font-size: 12px;
color: #64748b;
}
.soa-pct-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #f1f5f9;
color: #475569;
&--full,
&--ok {
background: #dcfce7;
color: #15803d;
}
&--warn {
background: #fef3c7;
color: #b45309;
}
}
.soa-sign-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
border: 1px solid #e2e8f0;
cursor: pointer;
transition: all 0.15s;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
&--mandatory {
background: #ede9fe;
color: #7c3aed;
border-color: #ddd6fe;
}
&--optional {
background: #f8fafc;
color: #94a3b8;
}
}
.soa-mandatory-count {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
color: #7c3aed;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
}
.soa-alert {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
}
&--warn {
background: #fef3c7;
color: #b45309;
}
&--ok {
background: #dcfce7;
color: #15803d;
}
}
.soa-empty {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
font-size: 13px;
color: #64748b;
mat-icon {
color: #94a3b8;
font-size: 22px;
height: 22px;
width: 22px;
}
}
.soa-add-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.soa-mode-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
@media (width <= 700px) {
grid-template-columns: repeat(2, 1fr);
}
}
.soa-mode-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: #a5b4fc;
}
&--selected {
border-color: #7c3aed;
background: #faf5ff;
}
&__icon {
font-size: 24px;
height: 24px;
width: 24px;
color: #7c3aed;
}
&__code {
font-size: 13px;
font-weight: 700;
color: #1f2937;
}
&__desc {
font-size: 11px;
color: #6b7280;
text-align: center;
}
&__check {
position: absolute;
top: 6px;
right: 6px;
font-size: 16px;
height: 16px;
width: 16px;
color: #7c3aed;
}
}
.soa-mode-summary {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
background: #f8fafc;
color: #64748b;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
}
&--set {
background: #faf5ff;
color: #7c3aed;
}
&__from {
font-size: 11px;
color: #94a3b8;
}
}
.soa-auto-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #fef9c3;
color: #854d0e;
border: 1px solid #fde68a;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
}
}
.soa-minor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px 16px;
margin-bottom: 16px;
@media (width <= 700px) {
grid-template-columns: 1fr;
}
}
.soa-field {
width: 100%;
}
.soa-guardian-search {
margin-bottom: 12px;
&__label {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
color: #7c3aed;
}
}
}
.soa-guardian-selected {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
margin-top: 10px;
&__icon {
font-size: 24px;
height: 24px;
width: 24px;
color: #16a34a;
}
&__name {
font-size: 13px;
font-weight: 600;
color: #1f2937;
}
&__cif {
font-size: 11px;
color: #6b7280;
}
}
.soa-sc-benefits {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 14px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.soa-sc-benefit {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
mat-icon {
color: #16a34a;
font-size: 20px;
height: 20px;
width: 20px;
flex-shrink: 0;
margin-top: 2px;
}
strong {
display: block;
font-size: 13px;
color: #1f2937;
font-weight: 600;
}
span {
font-size: 11px;
color: #6b7280;
}
}
.soa-category-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
@media (width <= 700px) {
grid-template-columns: repeat(2, 1fr);
}
}
.soa-cat-tile {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 10px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: #a5b4fc;
box-shadow: 0 2px 8px rgb(99 102 241 / 12%);
}
&--selected {
&.soa-cat-tile--male {
border-color: #3b82f6;
background: #eff6ff;
box-shadow: 0 2px 12px rgb(59 130 246 / 20%);
}
&.soa-cat-tile--female {
border-color: #ec4899;
background: #fdf2f8;
box-shadow: 0 2px 12px rgb(236 72 153 / 20%);
}
}
&__icon {
font-size: 26px;
height: 26px;
width: 26px;
color: #64748b;
}
&--selected.soa-cat-tile--male &__icon {
color: #3b82f6;
}
&--selected.soa-cat-tile--female &__icon {
color: #ec4899;
}
&__label {
font-size: 11px;
font-weight: 600;
color: #374151;
text-align: center;
line-height: 1.3;
}
&__check {
position: absolute;
top: 6px;
right: 6px;
font-size: 16px;
height: 16px;
width: 16px;
.soa-cat-tile--male & {
color: #3b82f6;
}
.soa-cat-tile--female & {
color: #ec4899;
}
}
}
.soa-info-banner {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
border-radius: 8px;
font-size: 12px;
background: #eff6ff;
border: 1px solid #bfdbfe;
color: #1e40af;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
flex-shrink: 0;
margin-top: 2px;
}
ul {
margin: 4px 0 0;
padding-left: 18px;
li {
margin-bottom: 3px;
}
}
&--minor {
background: #fefce8;
border-color: #fde68a;
color: #92400e;
}
&--senior {
background: #f0fdf4;
border-color: #bbf7d0;
color: #065f46;
}
&--male {
background: #eff6ff;
border-color: #bfdbfe;
color: #1e40af;
}
&--female {
background: #fdf2f8;
border-color: #f9a8d4;
color: #9d174d;
}
}
// ── Account Feature Toggles ────────────────────────────────────────────────
.sad-toggles {
display: flex;
@@ -8,7 +8,7 @@
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */
@@ -17,20 +17,7 @@ import { SettingsService } from 'app/settings/settings.service';
import { ClientsService } from 'app/clients/clients.service';
import { MatDialog } from '@angular/material/dialog';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatIcon } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
@@ -49,16 +36,6 @@ import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add
FaIconComponent,
MatStepperNext,
IntroducerSearchComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatIcon,
FormsModule
]
@@ -72,20 +49,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Product Data */
productData: any;
/** Field Officer Data */
fieldOfficerData: any;
/** For edit current account form */
isFieldOfficerPatched = false;
/** Current Account Details Form */
currentAccountDetailsForm: UntypedFormGroup;
currentAccountProductSelected = false;
@@ -93,7 +63,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
introducerName: string | null = null;
introducerCif: string | null = null;
/** Non-Individual section */
// ── Non-Individual (existing) ──────────────────────────────────────────
isNonIndividualClient = false;
relatedClients: Array<{
clientId: number;
@@ -111,16 +81,86 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
{ value: 'ONLY_ONE', label: 'Single', sublabel: 'Only 1 Person', icon: 'person' }
];
/** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter();
/** Local copy of product template for displaying classification info */
productTemplate: any = null;
accountTypeOptions: string[] = [];
customerSegmentOptions: string[] = [];
selectedAccountType: string | null = null;
selectedCustomerSegment: string | null = null;
// ── Client info (auto-fetched) ─────────────────────────────────────────
clientDob: Date | null = null;
clientAge: number | null = null;
clientGender: string | null = null;
primaryClientName = '';
primaryClientCif = '';
primaryClientId: number | null = null;
primaryHolderShare = 0;
// ── Status of Account (RBI) ────────────────────────────────────────────
readonly accountStatusOptions = [
{ value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' },
{ value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' },
{ value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' },
{ value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' },
{ value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' },
{ value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' }
];
selectedAccountStatus = '';
// ── Joint Account (Status of Account) ─────────────────────────────────
jointHolders: Array<{
clientId: number;
clientName: string;
clientCif: string;
sharePercentage: number;
isMandatory: boolean;
}> = [];
modeOfOperation = '';
readonly modeOfOperationOptions = [
{ value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' },
{ value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' },
{ value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' },
{ value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' }
];
// ── Individual Male — Special Category ────────────────────────────────
selectedSpecialCategory = '';
readonly specialCategoryOptions = [
{ value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' },
{ value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' },
{ value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' },
{ value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' },
{ value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' },
{ value: 'NONE', label: 'None / General', icon: 'person' }
];
// ── Individual Female — Weaker Section ────────────────────────────────
selectedWeakerSection = '';
readonly weakerSectionOptions = [
{ value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' },
{ value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' },
{ value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' },
{ value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' },
{ value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' },
{ value: 'NONE', label: 'None / General', icon: 'person_outline' }
];
// ── Minor Account ──────────────────────────────────────────────────────
guardianClientId: number | null = null;
guardianName = '';
guardianCif = '';
guardianRelationship = '';
minorDob: Date | null = null;
readonly guardianRelationshipOptions = ['Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian'];
constructor() {
this.createCurrentAccountDetailsForm();
}
@@ -128,9 +168,23 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.buildDependencies();
if (this.currentAccountTemplate?.clientId) {
this.clientsService.getClientData(String(this.currentAccountTemplate.clientId)).subscribe((client: any) => {
const clientId = this.currentAccountTemplate?.clientId;
if (clientId) {
this.primaryClientId = clientId;
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
this.isNonIndividualClient = client?.legalForm?.id === 2;
this.primaryClientName = client?.displayName ?? client?.fullname ?? '';
this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? '';
if (client?.dateOfBirth) {
const dob = this.parseClientDob(client.dateOfBirth);
if (dob) {
this.clientDob = dob;
this.clientAge = this.calculateAge(dob);
this.clientGender = client.gender?.name?.toLowerCase() ?? null;
this.autoDetectAccountStatus();
}
}
this.cdr.detectChanges();
});
}
@@ -153,39 +207,27 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}
}
/**
* Creates current account details form.
*/
createCurrentAccountDetailsForm() {
this.currentAccountDetailsForm = this.formBuilder.group({
productId: [
'',
Validators.required
],
submittedOnDate: [
'',
Validators.required
],
productId: ['', Validators.required],
submittedOnDate: ['', Validators.required],
fieldOfficerId: [''],
externalId: [''],
balanceSwapOption: [false],
msme: [false],
autoDebitFacility: [false]
autoDebitFacility: [false],
establishmentDate: [''],
startOperationDate: ['']
});
}
/**
* Fetches current account product template on productId value changes
*/
buildDependencies() {
const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId;
this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
this.currentAccountsService
.getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false)
.subscribe((response: any) => {
// Ensure currency is included in the product template
if (!response.currency) {
// Try to get currency from the selected product
const selectedProduct = this.currentAccountTemplate.productOptions?.find(
(product: any) => product.id == productId
);
@@ -203,7 +245,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
this.currentAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.currentAccountProductSelected = true;
// Pre-populate toggles from product defaults (user can override)
this.currentAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
@@ -220,9 +261,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
});
}
/**
* Formats template dates into the current CBS date format.
*/
private extractOptions(options: any[], fallback?: string): string[] {
if (Array.isArray(options) && options.length > 0) {
return options.map((o: any) => o.value ?? o.name ?? o);
@@ -231,9 +269,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}
formatTemplateDate(value: any): string {
if (!value) {
return '';
}
if (!value) return '';
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
@@ -248,15 +284,123 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
/**
* Returns current account form value.
*/
// ── Client DOB helpers ─────────────────────────────────────────────────
private parseClientDob(dob: any): Date | null {
if (!dob) return null;
if (Array.isArray(dob) && dob.length >= 3) return new Date(dob[0], dob[1] - 1, dob[2]);
const d = new Date(dob);
return isNaN(d.getTime()) ? null : d;
}
private calculateAge(dob: Date): number {
const today = this.settingsService.businessDate ?? new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
return age;
}
private autoDetectAccountStatus(): void {
if (this.clientAge === null) return;
if (this.clientAge < 18) {
this.selectedAccountStatus = 'MINOR';
this.minorDob = this.clientDob;
} else if (this.clientAge >= 60) {
this.selectedAccountStatus = 'SENIOR_CITIZEN';
} else if (this.clientGender === 'female') {
this.selectedAccountStatus = 'INDIVIDUAL_FEMALE';
} else {
this.selectedAccountStatus = 'INDIVIDUAL_MALE';
}
}
isTileDisabled(value: string): boolean {
if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18;
if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60;
return false;
}
getTileTooltip(value: string): string {
if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18)
return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`;
if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60)
return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`;
if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age';
if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age';
return '';
}
// ── Introducer ─────────────────────────────────────────────────────────
onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.introducerId = event?.clientId ?? null;
this.introducerName = event?.displayName ?? null;
this.introducerCif = event?.cifNumber ?? null;
}
onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.guardianClientId = event?.clientId ?? null;
this.guardianName = event?.displayName ?? '';
this.guardianCif = event?.cifNumber ?? '';
}
// ── Joint Account (Status of Account) ─────────────────────────────────
openAddJointHolderDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
panelClass: 'arc-dialog-panel',
disableClose: true
});
ref.afterClosed().subscribe((result) => {
if (result) {
const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif);
if (duplicate) {
alert(`Account holder with CIF ${result.clientCif} is already added.`);
return;
}
this.jointHolders = [...this.jointHolders, { ...result, isMandatory: false }];
this.autoSetModeOfOperation();
}
});
}
removeJointHolder(index: number): void {
this.jointHolders = this.jointHolders.filter((_, i) => i !== index);
this.autoSetModeOfOperation();
}
toggleJointHolderMandatory(holder: any): void {
holder.isMandatory = !holder.isMandatory;
this.autoSetModeOfOperation();
}
autoSetModeOfOperation(): void {
const mandatory = this.jointHolders.filter((h) => h.isMandatory).length;
if (mandatory === 0) this.modeOfOperation = 'EITHER_OR_SURVIVOR';
else if (mandatory === 1) this.modeOfOperation = 'FORMER_OR_SURVIVOR';
else this.modeOfOperation = 'JOINTLY';
}
get totalJointSharePercentage(): number {
return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0);
}
get mandatoryJointCount(): number {
return this.jointHolders.filter((h) => h.isMandatory).length;
}
get selectedModeLabel(): string {
return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? '';
}
get majorityDate(): string {
const dob = this.minorDob ?? this.clientDob;
if (!dob) return '';
const d = new Date(dob);
d.setFullYear(d.getFullYear() + 18);
return this.dateUtils.formatDate(d, 'M/d/yyyy') || '';
}
// ── Non-Individual (existing) helpers ──────────────────────────────────
openAddRelatedClientDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
@@ -270,9 +414,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
alert(`Partner with CIF ${result.clientCif} is already added.`);
return;
}
this.relatedClients = [
...this.relatedClients,
{ ...result, isMandatory: false }];
this.relatedClients = [...this.relatedClients, { ...result, isMandatory: false }];
this.autoSetSignatoryRights();
}
});
@@ -288,16 +430,11 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
this.autoSetSignatoryRights();
}
/** Auto-compute Mode of Operation based on mandatory count */
autoSetSignatoryRights(): void {
const mandatory = this.relatedClients.filter((c) => c.isMandatory).length;
if (mandatory === 0) {
this.signatoryRights = 'ANYONE';
} else if (mandatory === 1) {
this.signatoryRights = 'ONE_MANDATORY';
} else {
this.signatoryRights = 'TWO_MANDATORY';
}
if (mandatory === 0) this.signatoryRights = 'ANYONE';
else if (mandatory === 1) this.signatoryRights = 'ONE_MANDATORY';
else this.signatoryRights = 'TWO_MANDATORY';
}
get totalSharePercentage(): number {
@@ -329,6 +466,33 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
sharePercentage: c.sharePercentage,
isMandatory: c.isMandatory
}));
} else {
if (this.selectedAccountStatus) value.accountStatus = this.selectedAccountStatus;
if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory)
value.specialCategory = this.selectedSpecialCategory;
if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection)
value.weakerSection = this.selectedWeakerSection;
if (this.selectedAccountStatus === 'JOINT') {
value.modeOfOperation = this.modeOfOperation || null;
value.jointHolders = [
{
clientId: this.primaryClientId,
clientName: this.primaryClientName,
clientCif: this.primaryClientCif,
sharePercentage: this.primaryHolderShare,
isMandatory: true,
isPrimary: true
},
...this.jointHolders.map((h) => ({ ...h, isPrimary: false }))
];
}
if (this.selectedAccountStatus === 'MINOR') {
value.guardianClientId = this.guardianClientId;
value.guardianName = this.guardianName;
value.guardianCif = this.guardianCif;
value.guardianRelationship = this.guardianRelationship;
value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null;
}
}
return value;
}
@@ -68,6 +68,11 @@ export class CurrentAccountsService {
return this.http.get(`/currentaccounts/${accountId}`, { params: httpParams });
}
getClientCurrentAccounts(clientId: string): Observable<any> {
const httpParams = new HttpParams().set('clientId', clientId);
return this.http.get('/currentaccounts', { params: httpParams });
}
/**
* @param accountId Current Account Id of account to get data for.
* @returns {Observable<any>} Current account and template.
@@ -115,13 +115,8 @@
</mat-form-field>
}
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Introducer Required</mat-label>
<input matInput [value]="productTemplate.introducerRequired ? 'Yes' : 'No'" readonly />
</mat-form-field>
<div class="rad-field rad-info-row">
<span class="rad-info-label">Share Linking Enabled</span>
<span class="rad-info-label">Deposit Account Link</span>
<span class="rad-badge" [class.rad-badge--yes]="productTemplate.shareLinkingEnabled">
{{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }}
</span>
@@ -129,8 +124,652 @@
</div>
</div>
@if (productTemplate?.introducerRequired) {
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
<!-- ══ Deposit Account Link ══════════════════════════════════════════ -->
@if (productTemplate?.shareLinkingEnabled) {
<div class="dal-card">
<div class="dal-card__header">
<div class="dal-card__header-left">
<div class="dal-card__icon">
<mat-icon>link</mat-icon>
</div>
<div>
<h3 class="dal-card__title">Deposit Account Link</h3>
<span class="dal-card__sub">Link an existing Savings or Current account to this RD</span>
</div>
</div>
</div>
<div class="dal-card__body">
@if (dalLoading) {
<div class="dal-loading">
<mat-icon class="dal-loading__icon">hourglass_top</mat-icon>
<span>Loading accounts…</span>
</div>
} @else if (selectedLinkedAccount) {
<!-- ── Selected Account Card ── -->
<div class="dal-selected">
<div class="dal-selected__info">
<span class="dal-type-badge" [class.dal-type-badge--savings]="selectedLinkedAccount.accountType === 'SAVINGS'"
[class.dal-type-badge--current]="selectedLinkedAccount.accountType === 'CURRENT'">
<mat-icon>{{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'savings' : 'account_balance' }}</mat-icon>
{{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'Savings' : 'Current' }}
</span>
<div class="dal-selected__details">
<span class="dal-selected__accno">{{ selectedLinkedAccount.accountNo }}</span>
<span class="dal-selected__product">{{ selectedLinkedAccount.productName }}</span>
</div>
<div class="dal-selected__balance">
<span class="dal-selected__bal-label">Balance</span>
<span class="dal-selected__bal-amount">{{ selectedLinkedAccount.currencyCode }} {{ selectedLinkedAccount.balance | number:'1.2-2' }}</span>
</div>
</div>
<button mat-stroked-button color="warn" type="button" class="dal-change-btn" (click)="clearLinkedAccount()">
<mat-icon>swap_horiz</mat-icon> Change
</button>
</div>
} @else {
<!-- ── Step 1: Choose account type ── -->
@if (!dalSelectedType) {
<div class="dal-type-step">
<p class="dal-type-step__label">Select account type to link:</p>
<div class="dal-type-grid">
<button type="button" class="dal-type-btn dal-type-btn--savings" (click)="selectDalType('SAVINGS')">
<mat-icon>savings</mat-icon>
<span class="dal-type-btn__name">Savings Account</span>
<span class="dal-type-btn__count">{{ dalAllSavings.length }} account{{ dalAllSavings.length !== 1 ? 's' : '' }}</span>
</button>
<button type="button" class="dal-type-btn dal-type-btn--current" (click)="selectDalType('CURRENT')">
<mat-icon>account_balance</mat-icon>
<span class="dal-type-btn__name">Current Account</span>
<span class="dal-type-btn__count">{{ dalAllCurrent.length }} account{{ dalAllCurrent.length !== 1 ? 's' : '' }}</span>
</button>
</div>
</div>
} @else {
<!-- ── Step 2: Search & pick from accounts ── -->
<div class="dal-picker">
<div class="dal-picker__topbar">
<button type="button" mat-icon-button class="dal-back-btn" matTooltip="Back" (click)="dalSelectedType = null">
<mat-icon>arrow_back</mat-icon>
</button>
<span class="dal-picker__title">
<mat-icon>{{ dalSelectedType === 'SAVINGS' ? 'savings' : 'account_balance' }}</mat-icon>
{{ dalSelectedType === 'SAVINGS' ? 'Savings Accounts' : 'Current Accounts' }}
</span>
</div>
<mat-form-field appearance="outline" class="dal-search-field">
<mat-label>Search by Account No. or Product</mat-label>
<mat-icon matPrefix>search</mat-icon>
<input matInput [(ngModel)]="dalSearch" [ngModelOptions]="{standalone: true}" placeholder="Type to filter…" />
@if (dalSearch) {
<button matSuffix mat-icon-button type="button" (click)="dalSearch = ''">
<mat-icon>clear</mat-icon>
</button>
}
</mat-form-field>
@if (dalFilteredAccounts.length === 0) {
<div class="dal-empty">
<mat-icon>search_off</mat-icon>
<span>No {{ dalSelectedType === 'SAVINGS' ? 'Savings' : 'Current' }} accounts found.</span>
</div>
} @else {
<div class="dal-list">
@for (account of dalFilteredAccounts; track account.id) {
<div class="dal-item" (click)="selectLinkedAccount(account)">
<div class="dal-item__details">
<span class="dal-item__accno">{{ account.accountNo }}</span>
<span class="dal-item__product">{{ account.productName }}</span>
</div>
<div class="dal-item__right">
<span class="dal-item__status" [class.dal-item__status--active]="account.status === 'Active'">{{ account.status }}</span>
<span class="dal-item__balance">{{ account.currencyCode }} {{ account.balance | number:'1.2-2' }}</span>
</div>
<mat-icon class="dal-item__arrow">chevron_right</mat-icon>
</div>
}
</div>
}
</div>
}
}
</div>
</div>
}
}
<!-- ══ Status of Account (RBI) ═══════════════════════════════════════ -->
@if (isProductSelected) {
<div class="soa-card">
<div class="soa-card__header">
<div class="soa-card__header-left">
<div class="soa-card__icon">
<i class="fas fa-id-card"></i>
</div>
<div>
<h3 class="soa-card__title">Status of Account</h3>
<span class="soa-card__sub">As per RBI guidelines — select account holder type</span>
</div>
</div>
<span class="soa-pill">RBI Compliant</span>
</div>
<!-- Auto-detected client info bar -->
@if (clientDob) {
<div class="soa-client-info-bar">
<div class="soa-client-info-bar__item">
<mat-icon>cake</mat-icon>
<span>Date of Birth: <strong>{{ formatTemplateDate(clientDob) }}</strong></span>
</div>
<div class="soa-client-info-bar__item">
<mat-icon>person_pin</mat-icon>
<span>Age: <strong>{{ clientAge }} years</strong></span>
</div>
@if (clientAge !== null && clientAge < 18) {
<span class="soa-auto-pill soa-auto-pill--minor">
<mat-icon>child_care</mat-icon> Auto-detected: Minor
</span>
} @else if (clientAge !== null && clientAge >= 60) {
<span class="soa-auto-pill soa-auto-pill--senior">
<mat-icon>elderly</mat-icon> Auto-detected: Senior Citizen
</span>
} @else {
<span class="soa-auto-pill soa-auto-pill--ok"><mat-icon>check_circle</mat-icon> Age Verified</span>
}
</div>
}
<!-- Status selector tiles -->
<div class="soa-tiles">
@for (opt of accountStatusOptions; track opt.value) {
<button
type="button"
class="soa-tile"
[class.soa-tile--selected]="selectedAccountStatus === opt.value"
[class.soa-tile--disabled]="isTileDisabled(opt.value)"
[disabled]="isTileDisabled(opt.value)"
(click)="!isTileDisabled(opt.value) && (selectedAccountStatus = opt.value)"
[title]="getTileTooltip(opt.value)"
>
<mat-icon class="soa-tile__icon" [style.color]="isTileDisabled(opt.value) ? '#cbd5e1' : opt.color">
{{ opt.icon }}
</mat-icon>
<span class="soa-tile__label">{{ opt.label }}</span>
@if (isTileDisabled(opt.value)) {
<span class="soa-tile__lock"><mat-icon>lock</mat-icon></span>
}
@if (selectedAccountStatus === opt.value) {
<mat-icon class="soa-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<!-- ── JOINT ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'JOINT') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #8b5cf6">A</span>
<div>
<span class="soa-block__title">Joint Account Holders</span>
<span class="soa-block__hint">Add all co-applicants linked to this joint RD account</span>
</div>
</div>
<button mat-flat-button color="primary" type="button" class="soa-add-btn" (click)="openAddJointHolderDialog()">
<mat-icon>person_add</mat-icon> Add Holder
</button>
</div>
<div class="soa-table-wrap">
<table class="soa-tbl">
<thead>
<tr>
<th class="soa-th soa-th--sr">Sr.</th>
<th class="soa-th">Holder Name</th>
<th class="soa-th">CIF No.</th>
<th class="soa-th soa-th--center">Ownership %</th>
<th class="soa-th soa-th--center">Signing Authority</th>
<th class="soa-th soa-th--center">Role</th>
<th class="soa-th soa-th--center">Action</th>
</tr>
</thead>
<tbody>
<tr class="soa-tr soa-tr--primary">
<td class="soa-td soa-td--sr">1</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar soa-avatar--primary">
{{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }}
</div>
<div>
<span class="soa-client-name">{{ primaryClientName || 'Primary Holder' }}</span>
<span class="soa-primary-tag">Primary Account Holder</span>
</div>
</div>
</td>
<td class="soa-td"><span class="soa-mono">{{ primaryClientCif || '—' }}</span></td>
<td class="soa-td soa-td--center">
<input
class="soa-pct-input"
type="number"
min="0"
max="100"
[(ngModel)]="primaryHolderShare"
[ngModelOptions]="{ standalone: true }"
placeholder="0"
/>
<span class="soa-pct-unit">%</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-sign-toggle soa-sign-toggle--mandatory">
<mat-icon>lock</mat-icon> Mandatory
</span>
</td>
<td class="soa-td soa-td--center">Primary Account Holder</td>
<td class="soa-td soa-td--center"><span class="soa-no-action">—</span></td>
</tr>
@for (h of jointHolders; track h; let i = $index) {
<tr class="soa-tr" [class.soa-tr--mandatory]="h.isMandatory">
<td class="soa-td soa-td--sr">{{ i + 2 }}</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar" [class.soa-avatar--mandatory]="h.isMandatory">
{{ h.clientName[0].toUpperCase() }}
</div>
<div>
<span class="soa-client-name">{{ h.clientName }}</span>
@if (h.isMandatory) {
<span class="soa-mandatory-tag">Mandatory Signatory</span>
}
</div>
</div>
</td>
<td class="soa-td"><span class="soa-mono">{{ h.clientCif || '—' }}</span></td>
<td class="soa-td soa-td--center">
<span class="soa-pct-badge" [class.soa-pct-badge--full]="h.sharePercentage === 100">
{{ h.sharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<button
type="button"
class="soa-sign-toggle"
[class.soa-sign-toggle--mandatory]="h.isMandatory"
[class.soa-sign-toggle--optional]="!h.isMandatory"
(click)="toggleJointHolderMandatory(h)"
>
<mat-icon>{{ h.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
</button>
</td>
<td class="soa-td soa-td--center">Co-Holder</td>
<td class="soa-td soa-td--center">
<button mat-icon-button color="warn" type="button" (click)="removeJointHolder(i)">
<mat-icon>delete_outline</mat-icon>
</button>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="soa-tr-total">
<td colspan="3" class="soa-td soa-td--total-label">Total Ownership</td>
<td class="soa-td soa-td--center">
<span
class="soa-pct-badge"
[class.soa-pct-badge--ok]="totalJointSharePercentage === 100"
[class.soa-pct-badge--warn]="totalJointSharePercentage !== 100"
>
{{ totalJointSharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-mandatory-count">
<mat-icon>lock</mat-icon> {{ mandatoryJointCount + 1 }} Mandatory
</span>
</td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty" style="margin-top: 8px">
<mat-icon>group_add</mat-icon>
<span>Click <strong>Add Holder</strong> to add joint account holders.</span>
</div>
}
@if (totalJointSharePercentage !== 100) {
<div class="soa-alert soa-alert--warn" style="margin-top: 8px">
<mat-icon>warning_amber</mat-icon>
Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%.
</div>
} @else {
<div class="soa-alert soa-alert--ok" style="margin-top: 8px">
<mat-icon>check_circle</mat-icon>
Ownership allocation is complete (100%).
</div>
}
</div>
<div class="soa-block soa-block--sep">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #7c3aed">B</span>
<div>
<span class="soa-block__title">Mode of Operation</span>
<span class="soa-block__hint">Auto-set from mandatory holders — override manually if required</span>
</div>
</div>
@if (modeOfOperation) {
<span class="soa-auto-badge"><mat-icon>auto_awesome</mat-icon> Auto</span>
}
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty">
<mat-icon>how_to_vote</mat-icon>
<span>Add joint holders in Section A first.</span>
</div>
} @else {
<div class="soa-mode-grid">
@for (opt of modeOfOperationOptions; track opt.value) {
<button
type="button"
class="soa-mode-card"
[class.soa-mode-card--selected]="modeOfOperation === opt.value"
(click)="modeOfOperation = opt.value"
>
<mat-icon class="soa-mode-card__icon">{{ opt.icon }}</mat-icon>
<div class="soa-mode-card__code">{{ opt.label }}</div>
<div class="soa-mode-card__desc">{{ opt.sublabel }}</div>
@if (modeOfOperation === opt.value) {
<mat-icon class="soa-mode-card__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-mode-summary" [class.soa-mode-summary--set]="modeOfOperation">
@if (modeOfOperation) {
<mat-icon>how_to_vote</mat-icon>
<span>Mode of Operation: <strong>{{ selectedModeLabel }}</strong></span>
<span class="soa-mode-summary__from">
({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional)
</span>
} @else {
<mat-icon>info</mat-icon>
<span>Set Mandatory/Optional for holders above to auto-select mode.</span>
}
</div>
}
</div>
}
<!-- ── MINOR ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'MINOR') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #f59e0b">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">child_care</mat-icon>
</span>
<div>
<span class="soa-block__title">Minor Account Details</span>
<span class="soa-block__hint">As per RBI — guardian must be an existing registered client</span>
</div>
</div>
<span class="soa-pill soa-pill--minor">Guardian Required</span>
</div>
<div class="soa-minor-grid">
<mat-form-field appearance="outline" class="soa-field" (click)="minorDobPicker.open()">
<mat-label>Minor's Date of Birth</mat-label>
<input
matInput
[matDatepicker]="minorDobPicker"
[max]="maxDate"
[(ngModel)]="minorDob"
[ngModelOptions]="{ standalone: true }"
/>
<mat-datepicker-toggle matSuffix [for]="minorDobPicker"></mat-datepicker-toggle>
<mat-datepicker #minorDobPicker></mat-datepicker>
@if (clientDob) {
<mat-hint>Auto-filled from client profile</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Must be below 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Majority (Auto)</mat-label>
<input matInput [value]="majorityDate" readonly />
<mat-hint>Auto-calculated: DOB + 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Guardian Relationship</mat-label>
<mat-select [(ngModel)]="guardianRelationship" [ngModelOptions]="{ standalone: true }">
@for (rel of guardianRelationshipOptions; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="soa-guardian-search">
<div class="soa-guardian-search__label">
<mat-icon>manage_accounts</mat-icon>
<span>Search &amp; Link Guardian (existing client)</span>
</div>
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
@if (guardianName) {
<div class="soa-guardian-selected">
<mat-icon class="soa-guardian-selected__icon">verified_user</mat-icon>
<div>
<div class="soa-guardian-selected__name">{{ guardianName }}</div>
<div class="soa-guardian-selected__cif">CIF: {{ guardianCif || 'N/A' }}</div>
</div>
<span class="soa-pill soa-pill--ok">Guardian Linked</span>
</div>
}
</div>
<div class="soa-info-banner soa-info-banner--minor">
<mat-icon>info</mat-icon>
<ul>
<li>Minor below 10 years — Guardian must operate the account.</li>
<li>Minor above 10 years — Can operate independently (bank discretion).</li>
<li>Account must be converted to regular savings on attaining majority.</li>
<li>Fresh KYC of the minor must be done on majority.</li>
</ul>
</div>
</div>
}
<!-- ── SENIOR CITIZEN ────────────────────────────────────────── -->
@if (selectedAccountStatus === 'SENIOR_CITIZEN') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #10b981">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">elderly</mat-icon>
</span>
<div>
<span class="soa-block__title">Senior Citizen Account</span>
<span class="soa-block__hint">Age ≥ 60 years — eligible for preferential banking services</span>
</div>
</div>
<span class="soa-pill soa-pill--senior">Age ≥ 60</span>
</div>
<div class="soa-minor-grid" style="margin-bottom: 16px">
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Birth</mat-label>
<input matInput [value]="formatTemplateDate(clientDob)" readonly />
<mat-hint>From client profile</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Age ≥ 60 required for Senior Citizen</mat-hint>
</mat-form-field>
</div>
<div class="soa-sc-benefits">
<div class="soa-sc-benefit">
<mat-icon>priority_high</mat-icon>
<div>
<strong>Priority Service</strong>
<span>Dedicated counters / assisted banking at branches</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>percent</mat-icon>
<div>
<strong>Preferential Interest Rate</strong>
<span>Additional 0.25%–0.50% on deposits (as per product)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>health_and_safety</mat-icon>
<div>
<strong>Doorstep Banking</strong>
<span>Eligible for home banking services (RBI circular)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>gavel</mat-icon>
<div>
<strong>Nomination Mandatory</strong>
<span>RBI mandate — ensure nomination is registered</span>
</div>
</div>
</div>
<div class="soa-info-banner soa-info-banner--senior">
<mat-icon>info</mat-icon>
<ul>
<li>Age proof (DOB document) mandatory at time of account opening.</li>
<li>Nomination registration is compulsory (Form DA-1).</li>
<li>Bank must offer dedicated helpline and priority grievance redressal.</li>
<li>Door-step banking services must be offered (RBI Master Direction 2023).</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL MALE ───────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_MALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #3b82f6">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Male)</span>
<span class="soa-block__hint">Select special category if applicable — governs applicable schemes &amp; benefits</span>
</div>
</div>
<span class="soa-pill" style="background: #dbeafe; color: #1d4ed8; border-color: #bfdbfe">Special Category</span>
</div>
<div class="soa-category-grid">
@for (opt of specialCategoryOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value"
[class.soa-cat-tile--male]="true"
(click)="selectedSpecialCategory = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedSpecialCategory === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--male">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).</li>
<li>NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL FEMALE ──────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #ec4899">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person_outline</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Female)</span>
<span class="soa-block__hint">Select weaker section category if applicable — governs PMJDY &amp; priority schemes</span>
</div>
</div>
<span class="soa-pill" style="background: #fce7f3; color: #be185d; border-color: #f9a8d4">Weaker Section</span>
</div>
<div class="soa-category-grid">
@for (opt of weakerSectionOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedWeakerSection === opt.value"
[class.soa-cat-tile--female]="true"
(click)="selectedWeakerSection = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedWeakerSection === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--female">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).</li>
<li>PMJDY beneficiary — ₹10,000 overdraft facility &amp; accident insurance cover applicable.</li>
<li>SHG members — group linkage lending &amp; special deposit rates applicable.</li>
</ul>
</div>
</div>
}
</div>
}
</form>
@@ -11,14 +11,17 @@ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorR
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { MatDialog } from '@angular/material/dialog';
/** Custom Services */
import { RecurringDepositsService } from '../../recurring-deposits.service';
import { ClientsService } from 'app/clients/clients.service';
import { MatTooltip } from '@angular/material/tooltip';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
import { MatIcon } from '@angular/material/icon';
import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component';
/**
* Recurring Deposits Account Details Step
@@ -33,8 +36,8 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
MatStepperPrevious,
FaIconComponent,
MatStepperNext,
IntroducerSearchComponent,
FormsModule
FormsModule,
MatIcon
]
})
export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
@@ -43,6 +46,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
private clientsService = inject(ClientsService);
private dialog = inject(MatDialog);
/** Recurring Deposits Account Template */
@Input() recurringDepositsAccountTemplate: any;
@@ -61,9 +66,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
recurringDepositAccountDetailsForm!: UntypedFormGroup;
isProductSelected = false;
introducerId: number | null = null;
introducerName: string | null = null;
introducerCif: string | null = null;
/** Recurring Deposits Account Template with product data */
@Output() recurringDepositsAccountProductTemplate = new EventEmitter();
@@ -75,6 +77,145 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
selectedAccountType: string | null = null;
selectedCustomerSegment: string | null = null;
// ── Client info (auto-fetched) ─────────────────────────────────────────
clientDob: Date | null = null;
clientAge: number | null = null;
clientGender: string | null = null;
primaryClientName = '';
primaryClientCif = '';
primaryClientId: number | null = null;
primaryHolderShare = 0;
// ── Status of Account (RBI) ────────────────────────────────────────────
readonly accountStatusOptions = [
{ value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' },
{ value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' },
{ value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' },
{ value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' },
{ value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' },
{ value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' }
];
selectedAccountStatus = '';
// ── Joint Account ──────────────────────────────────────────────────────
jointHolders: Array<{
clientId: number;
clientName: string;
clientCif: string;
sharePercentage: number;
isMandatory: boolean;
}> = [];
modeOfOperation = '';
readonly modeOfOperationOptions = [
{ value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' },
{ value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' },
{ value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' },
{ value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' }
];
// ── Individual Male — Special Category ────────────────────────────────
selectedSpecialCategory = '';
readonly specialCategoryOptions = [
{ value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' },
{ value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' },
{ value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' },
{ value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' },
{ value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' },
{ value: 'NONE', label: 'None / General', icon: 'person' }
];
// ── Individual Female — Weaker Section ────────────────────────────────
selectedWeakerSection = '';
readonly weakerSectionOptions = [
{ value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' },
{ value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' },
{ value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' },
{ value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' },
{ value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' },
{ value: 'NONE', label: 'None / General', icon: 'person_outline' }
];
// ── Minor Account ──────────────────────────────────────────────────────
guardianClientId: number | null = null;
guardianName = '';
guardianCif = '';
guardianRelationship = '';
minorDob: Date | null = null;
readonly guardianRelationshipOptions = ['Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian'];
// ── Deposit Account Link ───────────────────────────────────────────────
dalSelectedType: 'SAVINGS' | 'CURRENT' | null = null;
dalAllSavings: any[] = [];
dalAllCurrent: any[] = [];
dalSearch = '';
dalLoading = false;
selectedLinkedAccount: any = null;
get dalAccounts(): any[] {
return this.dalSelectedType === 'SAVINGS' ? this.dalAllSavings : this.dalAllCurrent;
}
get dalFilteredAccounts(): any[] {
const q = this.dalSearch.toLowerCase().trim();
if (!q) return this.dalAccounts;
return this.dalAccounts.filter(
(a) => a.accountNo.toLowerCase().includes(q) || a.productName.toLowerCase().includes(q)
);
}
selectDalType(type: 'SAVINGS' | 'CURRENT'): void {
this.dalSelectedType = type;
this.dalSearch = '';
this.selectedLinkedAccount = null;
}
selectLinkedAccount(account: any): void {
this.selectedLinkedAccount = account;
}
clearLinkedAccount(): void {
this.selectedLinkedAccount = null;
this.dalSelectedType = null;
this.dalSearch = '';
}
private loadClientAccounts(): void {
const clientId = String(this.recurringDepositsAccountTemplate?.clientId);
if (!clientId) return;
this.dalLoading = true;
this.clientsService.getClientAccountData(clientId).subscribe({
next: (data: any) => {
this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({
id: a.id,
accountNo: a.accountNo,
productName: a.productName ?? '',
accountType: 'SAVINGS' as const,
status: a.status?.value ?? '',
balance: a.accountBalance ?? 0,
currencyCode: a.currency?.code ?? ''
}));
this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({
id: a.id,
accountNo: a.accountNo,
productName: a.productName ?? '',
accountType: 'CURRENT' as const,
status: a.status?.value ?? '',
balance: a.accountBalance ?? 0,
currencyCode: a.currency?.code ?? ''
}));
this.dalLoading = false;
this.cdr.detectChanges();
},
error: () => { this.dalLoading = false; }
});
}
constructor() {
this.createRecurringDepositsAccountDetailsForm();
}
@@ -85,7 +226,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
if (this.recurringDepositsAccountTemplate) {
this.productData = this.recurringDepositsAccountTemplate.productOptions;
if (this.recurringDepositsAccountTemplate.depositProductId) {
// Edit mode: patch from saved data
this.recurringDepositAccountDetailsForm.patchValue({
productId: this.recurringDepositsAccountTemplate.depositProductId,
submittedOnDate:
@@ -94,12 +234,84 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
externalId: this.recurringDepositsAccountTemplate.externalId
});
} else {
// New account: default submittedOnDate to today's business date
this.recurringDepositAccountDetailsForm.patchValue({
submittedOnDate: this.settingsService.businessDate
});
}
}
// Auto-detect account status from client DOB + capture primary holder info
const clientId = this.recurringDepositsAccountTemplate?.clientId;
if (clientId) {
this.primaryClientId = clientId;
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
this.primaryClientName = client?.displayName ?? client?.fullname ?? '';
this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? '';
if (client?.dateOfBirth) {
const dob = this.parseClientDob(client.dateOfBirth);
if (dob) {
this.clientDob = dob;
this.clientAge = this.calculateAge(dob);
this.clientGender = client.gender?.name?.toLowerCase() ?? null;
this.autoDetectAccountStatus();
}
}
this.cdr.detectChanges();
});
}
}
/** Parse DOB from backend — handles array [yyyy, mm, dd] or string */
private parseClientDob(dob: any): Date | null {
if (!dob) return null;
if (Array.isArray(dob) && dob.length >= 3) {
return new Date(dob[0], dob[1] - 1, dob[2]);
}
const d = new Date(dob);
return isNaN(d.getTime()) ? null : d;
}
/** Calculate full years from a date to today */
private calculateAge(dob: Date): number {
const today = this.settingsService.businessDate ?? new Date();
let age = today.getFullYear() - dob.getFullYear();
const monthDiff = today.getMonth() - dob.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
age--;
}
return age;
}
isTileDisabled(value: string): boolean {
if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18;
if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60;
return false;
}
getTileTooltip(value: string): string {
if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) {
return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`;
}
if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) {
return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`;
}
if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age';
if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age';
return '';
}
private autoDetectAccountStatus(): void {
if (this.clientAge === null) return;
if (this.clientAge < 18) {
this.selectedAccountStatus = 'MINOR';
this.minorDob = this.clientDob;
} else if (this.clientAge >= 60) {
this.selectedAccountStatus = 'SENIOR_CITIZEN';
} else if (this.clientGender === 'female') {
this.selectedAccountStatus = 'INDIVIDUAL_FEMALE';
} else {
this.selectedAccountStatus = 'INDIVIDUAL_MALE';
}
}
/**
@@ -107,14 +319,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
*/
createRecurringDepositsAccountDetailsForm() {
this.recurringDepositAccountDetailsForm = this.formBuilder.group({
productId: [
'',
Validators.required
],
submittedOnDate: [
'',
Validators.required
],
productId: ['', Validators.required],
submittedOnDate: ['', Validators.required],
fieldOfficerId: [''],
externalId: [''],
balanceSwapOption: [false],
@@ -132,9 +338,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.recurringDepositsService
.getRecurringDepositsAccountTemplate(clientId, productId)
.subscribe((response: any) => {
// Ensure currency is included in the product template
if (!response.currency) {
// Try to get currency from the selected product
const selectedProduct = this.recurringDepositsAccountTemplate.productOptions?.find(
(product: any) => product.id == productId
);
@@ -152,7 +356,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.recurringDepositsAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.isProductSelected = true;
// Pre-populate toggles from product defaults (user can override)
if (response.shareLinkingEnabled) {
this.loadClientAccounts();
}
this.recurringDepositAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
@@ -171,17 +377,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
});
}
/**
* Returns true if a value is not null, undefined, or empty string.
*/
hasValue(val: any): boolean {
return val !== null && val !== undefined && val !== '';
}
/**
* Formats template dates into the current CBS date format.
* Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects.
*/
private extractOptions(options: any[], fallback?: string): string[] {
if (Array.isArray(options) && options.length > 0) {
return options.map((o: any) => o.value ?? o.name ?? o);
@@ -190,12 +389,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
}
formatTemplateDate(value: any): string {
if (!value) {
return '';
}
if (!value) return '';
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
} else if (Array.isArray(value) && value.length >= 3) {
@@ -203,30 +398,133 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
} else {
dateValue = new Date(value);
}
if (!dateValue || Number.isNaN(dateValue.getTime())) {
return Array.isArray(value) ? value.join('/') : String(value);
}
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.guardianClientId = event?.clientId ?? null;
this.guardianName = event?.displayName ?? '';
this.guardianCif = event?.cifNumber ?? '';
}
// ── Joint Account helpers ──────────────────────────────────────────────
openAddJointHolderDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
panelClass: 'arc-dialog-panel',
disableClose: true
});
ref.afterClosed().subscribe((result) => {
if (result) {
const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif);
if (duplicate) {
alert(`Account holder with CIF ${result.clientCif} is already added.`);
return;
}
this.jointHolders = [...this.jointHolders, { ...result, isMandatory: false }];
this.autoSetModeOfOperation();
}
});
}
removeJointHolder(index: number): void {
this.jointHolders = this.jointHolders.filter((_, i) => i !== index);
this.autoSetModeOfOperation();
}
toggleJointHolderMandatory(holder: any): void {
holder.isMandatory = !holder.isMandatory;
this.autoSetModeOfOperation();
}
autoSetModeOfOperation(): void {
const mandatory = this.jointHolders.filter((h) => h.isMandatory).length;
if (mandatory === 0) {
this.modeOfOperation = 'EITHER_OR_SURVIVOR';
} else if (mandatory === 1) {
this.modeOfOperation = 'FORMER_OR_SURVIVOR';
} else {
this.modeOfOperation = 'JOINTLY';
}
}
get totalJointSharePercentage(): number {
return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0);
}
get mandatoryJointCount(): number {
return this.jointHolders.filter((h) => h.isMandatory).length;
}
get selectedModeLabel(): string {
return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? '';
}
get majorityDate(): string {
const dob = this.minorDob ?? this.clientDob;
if (!dob) return '';
const d = new Date(dob);
d.setFullYear(d.getFullYear() + 18);
return this.dateUtils.formatDate(d, 'M/d/yyyy') || '';
}
/**
* Returns recurring deposits account details form value.
*/
onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.introducerId = event?.clientId ?? null;
this.introducerName = event?.displayName ?? null;
this.introducerCif = event?.cifNumber ?? null;
}
get recurringDepositAccountDetails() {
const value = this.recurringDepositAccountDetailsForm.value;
if (this.introducerId) {
value.introducerId = this.introducerId;
value.introducerName = this.introducerName;
value.introducerCif = this.introducerCif;
if (this.selectedAccountStatus) {
value.accountStatus = this.selectedAccountStatus;
}
if (this.selectedAccountStatus === 'JOINT') {
value.modeOfOperation = this.modeOfOperation || null;
value.jointHolders = [
{
clientId: this.primaryClientId,
clientName: this.primaryClientName,
clientCif: this.primaryClientCif,
sharePercentage: this.primaryHolderShare,
isMandatory: true,
isPrimary: true
},
...this.jointHolders.map((h) => ({
clientId: h.clientId,
clientName: h.clientName,
clientCif: h.clientCif,
sharePercentage: h.sharePercentage,
isMandatory: h.isMandatory,
isPrimary: false
}))
];
}
if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) {
value.specialCategory = this.selectedSpecialCategory;
}
if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) {
value.weakerSection = this.selectedWeakerSection;
}
if (this.selectedAccountStatus === 'MINOR') {
value.guardianClientId = this.guardianClientId;
value.guardianName = this.guardianName;
value.guardianCif = this.guardianCif;
value.guardianRelationship = this.guardianRelationship;
value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null;
}
if (this.selectedLinkedAccount) {
value.linkedAccountId = this.selectedLinkedAccount.id;
value.linkedAccountType = this.selectedLinkedAccount.accountType;
value.linkedAccountNo = this.selectedLinkedAccount.accountNo;
}
return value;
}
}
@@ -112,7 +112,7 @@
recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== null
) {
<div class="preview-field">
<span class="preview-label">Share Linking Enabled</span>
<span class="preview-label">Deposit Account Link</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositsAccountProductTemplate.shareLinkingEnabled"
@@ -120,7 +120,7 @@
</mat-form-field>
<div class="rdpd-field rdpd-field--full rdpd-checkbox-row">
<mat-checkbox formControlName="shareLinkingEnabled">Share Linking Enabled</mat-checkbox>
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
</div>
</div>
</div>
@@ -87,7 +87,7 @@
@if (recurringDepositProduct.shareLinkingEnabled !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Share Linking Enabled:</span>
<span class="flex-40">Deposit Account Link:</span>
<span class="flex-60">{{ recurringDepositProduct.shareLinkingEnabled | yesNo }}</span>
</div>
}
@@ -77,7 +77,7 @@
<span class="flex-60">{{ recurringDepositProduct.introducerRequired | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Share Linking Enabled:</span>
<span class="flex-40">Deposit Account Link:</span>
<span class="flex-60">{{ recurringDepositProduct.shareLinkingEnabled | yesNo }}</span>
</div>
@@ -113,7 +113,7 @@ export class CreateSavingsAccountComponent {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM';
const savingsAccount = {
const savingsAccount: any = {
...this.savingsAccount,
charges: [],
submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat),
@@ -121,6 +121,17 @@ export class CreateSavingsAccountComponent {
monthDayFormat,
locale
};
// Digital Facilities
if (this.savingsAccountDigital?.accountFacilityConfig) {
savingsAccount.accountFacilityConfig = this.savingsAccountDigital.accountFacilityConfig;
}
// Nominee
if (this.savingsAccountNominee) {
savingsAccount.nominee = this.savingsAccountNominee;
}
if (this.savingsAccountTemplate.clientId) {
savingsAccount.clientId = this.savingsAccountTemplate.clientId;
} else {
@@ -135,4 +135,561 @@
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
}
}
<!-- ══ Status of Account (RBI) ═══════════════════════════════════════ -->
@if (savingsProductSelected) {
<div class="soa-card">
<div class="soa-card__header">
<div class="soa-card__header-left">
<div class="soa-card__icon">
<i class="fas fa-id-card"></i>
</div>
<div>
<h3 class="soa-card__title">Status of Account</h3>
<span class="soa-card__sub">As per RBI guidelines — select account holder type</span>
</div>
</div>
<span class="soa-pill">RBI Compliant</span>
</div>
<!-- Auto-detected client info bar -->
@if (clientDob) {
<div class="soa-client-info-bar">
<div class="soa-client-info-bar__item">
<mat-icon>cake</mat-icon>
<span
>Date of Birth: <strong>{{ formatTemplateDate(clientDob) }}</strong></span
>
</div>
<div class="soa-client-info-bar__item">
<mat-icon>person_pin</mat-icon>
<span
>Age: <strong>{{ clientAge }} years</strong></span
>
</div>
@if (clientAge !== null && clientAge < 18) {
<span class="soa-auto-pill soa-auto-pill--minor">
<mat-icon>child_care</mat-icon> Auto-detected: Minor
</span>
} @else if (clientAge !== null && clientAge >= 60) {
<span class="soa-auto-pill soa-auto-pill--senior">
<mat-icon>elderly</mat-icon> Auto-detected: Senior Citizen
</span>
} @else {
<span class="soa-auto-pill soa-auto-pill--ok"> <mat-icon>check_circle</mat-icon> Age Verified </span>
}
</div>
}
<!-- Status selector tiles -->
<div class="soa-tiles">
@for (opt of accountStatusOptions; track opt.value) {
<button
type="button"
class="soa-tile"
[class.soa-tile--selected]="selectedAccountStatus === opt.value"
[class.soa-tile--disabled]="isTileDisabled(opt.value)"
[disabled]="isTileDisabled(opt.value)"
(click)="!isTileDisabled(opt.value) && (selectedAccountStatus = opt.value)"
[title]="getTileTooltip(opt.value)"
>
<mat-icon class="soa-tile__icon" [style.color]="isTileDisabled(opt.value) ? '#cbd5e1' : opt.color">
{{ opt.icon }}
</mat-icon>
<span class="soa-tile__label">{{ opt.label }}</span>
@if (isTileDisabled(opt.value)) {
<span class="soa-tile__lock"><mat-icon>lock</mat-icon></span>
}
@if (selectedAccountStatus === opt.value) {
<mat-icon class="soa-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<!-- ── JOINT ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'JOINT') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #8b5cf6">A</span>
<div>
<span class="soa-block__title">Joint Account Holders</span>
<span class="soa-block__hint">Add all co-applicants linked to this joint savings account</span>
</div>
</div>
<button
mat-flat-button
color="primary"
type="button"
class="soa-add-btn"
(click)="openAddJointHolderDialog()"
>
<mat-icon>person_add</mat-icon> Add Holder
</button>
</div>
<div class="soa-table-wrap">
<table class="soa-tbl">
<thead>
<tr>
<th class="soa-th soa-th--sr">Sr.</th>
<th class="soa-th">Holder Name</th>
<th class="soa-th">CIF No.</th>
<th class="soa-th soa-th--center">Ownership %</th>
<th class="soa-th soa-th--center">Signing Authority</th>
<th class="soa-th soa-th--center">Role</th>
<th class="soa-th soa-th--center">Action</th>
</tr>
</thead>
<tbody>
<!-- Primary account holder row (non-removable) -->
<tr class="soa-tr soa-tr--primary">
<td class="soa-td soa-td--sr">1</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar soa-avatar--primary">
{{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }}
</div>
<div>
<span class="soa-client-name">{{ primaryClientName || 'Primary Holder' }}</span>
<span class="soa-primary-tag">Primary Account Holder</span>
</div>
</div>
</td>
<td class="soa-td">
<span class="soa-mono">{{ primaryClientCif || '—' }}</span>
</td>
<td class="soa-td soa-td--center">
<input
class="soa-pct-input"
type="number"
min="0"
max="100"
[(ngModel)]="primaryHolderShare"
[ngModelOptions]="{ standalone: true }"
placeholder="0"
/>
<span class="soa-pct-unit">%</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-sign-toggle soa-sign-toggle--mandatory">
<mat-icon>lock</mat-icon> Mandatory
</span>
</td>
<td class="soa-td soa-td--center">Primary Account Holder</td>
<td class="soa-td soa-td--center">
<span class="soa-no-action">—</span>
</td>
</tr>
<!-- Co-holders -->
@for (h of jointHolders; track h; let i = $index) {
<tr class="soa-tr" [class.soa-tr--mandatory]="h.isMandatory">
<td class="soa-td soa-td--sr">{{ i + 2 }}</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar" [class.soa-avatar--mandatory]="h.isMandatory">
{{ h.clientName[0].toUpperCase() }}
</div>
<div>
<span class="soa-client-name">{{ h.clientName }}</span>
@if (h.isMandatory) {
<span class="soa-mandatory-tag">Mandatory Signatory</span>
}
</div>
</div>
</td>
<td class="soa-td">
<span class="soa-mono">{{ h.clientCif || '—' }}</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-pct-badge" [class.soa-pct-badge--full]="h.sharePercentage === 100">
{{ h.sharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<button
type="button"
class="soa-sign-toggle"
[class.soa-sign-toggle--mandatory]="h.isMandatory"
[class.soa-sign-toggle--optional]="!h.isMandatory"
(click)="toggleJointHolderMandatory(h)"
>
<mat-icon>{{ h.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
</button>
</td>
<td class="soa-td soa-td--center">Co-Holder</td>
<td class="soa-td soa-td--center">
<button mat-icon-button color="warn" type="button" (click)="removeJointHolder(i)">
<mat-icon>delete_outline</mat-icon>
</button>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="soa-tr-total">
<td colspan="3" class="soa-td soa-td--total-label">Total Ownership</td>
<td class="soa-td soa-td--center">
<span
class="soa-pct-badge"
[class.soa-pct-badge--ok]="totalJointSharePercentage === 100"
[class.soa-pct-badge--warn]="totalJointSharePercentage !== 100"
>
{{ totalJointSharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-mandatory-count">
<mat-icon>lock</mat-icon> {{ mandatoryJointCount + 1 }} Mandatory
</span>
</td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty" style="margin-top: 8px">
<mat-icon>group_add</mat-icon>
<span>Click <strong>Add Holder</strong> to add joint account holders.</span>
</div>
}
@if (totalJointSharePercentage !== 100) {
<div class="soa-alert soa-alert--warn" style="margin-top: 8px">
<mat-icon>warning_amber</mat-icon>
Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%.
</div>
} @else {
<div class="soa-alert soa-alert--ok" style="margin-top: 8px">
<mat-icon>check_circle</mat-icon>
Ownership allocation is complete (100%).
</div>
}
</div>
<!-- Mode of Operation -->
<div class="soa-block soa-block--sep">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #7c3aed">B</span>
<div>
<span class="soa-block__title">Mode of Operation</span>
<span class="soa-block__hint">Auto-set from mandatory holders — override manually if required</span>
</div>
</div>
@if (modeOfOperation) {
<span class="soa-auto-badge"><mat-icon>auto_awesome</mat-icon> Auto</span>
}
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty">
<mat-icon>how_to_vote</mat-icon>
<span>Add joint holders in Section A first.</span>
</div>
} @else {
<div class="soa-mode-grid">
@for (opt of modeOfOperationOptions; track opt.value) {
<button
type="button"
class="soa-mode-card"
[class.soa-mode-card--selected]="modeOfOperation === opt.value"
(click)="modeOfOperation = opt.value"
>
<mat-icon class="soa-mode-card__icon">{{ opt.icon }}</mat-icon>
<div class="soa-mode-card__code">{{ opt.label }}</div>
<div class="soa-mode-card__desc">{{ opt.sublabel }}</div>
@if (modeOfOperation === opt.value) {
<mat-icon class="soa-mode-card__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-mode-summary" [class.soa-mode-summary--set]="modeOfOperation">
@if (modeOfOperation) {
<mat-icon>how_to_vote</mat-icon>
<span
>Mode of Operation: <strong>{{ selectedModeLabel }}</strong></span
>
<span class="soa-mode-summary__from">
({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional)
</span>
} @else {
<mat-icon>info</mat-icon>
<span>Set Mandatory/Optional for holders above to auto-select mode.</span>
}
</div>
}
</div>
}
<!-- ── MINOR ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'MINOR') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #f59e0b">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">child_care</mat-icon>
</span>
<div>
<span class="soa-block__title">Minor Account Details</span>
<span class="soa-block__hint">As per RBI — guardian must be an existing registered client</span>
</div>
</div>
<span class="soa-pill soa-pill--minor">Guardian Required</span>
</div>
<div class="soa-minor-grid">
<mat-form-field appearance="outline" class="soa-field" (click)="minorDobPicker.open()">
<mat-label>Minor's Date of Birth</mat-label>
<input
matInput
[matDatepicker]="minorDobPicker"
[max]="maxDate"
[(ngModel)]="minorDob"
[ngModelOptions]="{ standalone: true }"
/>
<mat-datepicker-toggle matSuffix [for]="minorDobPicker"></mat-datepicker-toggle>
<mat-datepicker #minorDobPicker></mat-datepicker>
@if (clientDob) {
<mat-hint>Auto-filled from client profile</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Must be below 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Majority (Auto)</mat-label>
<input matInput [value]="majorityDate" readonly />
<mat-hint>Auto-calculated: DOB + 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Guardian Relationship</mat-label>
<mat-select [(ngModel)]="guardianRelationship" [ngModelOptions]="{ standalone: true }">
@for (rel of guardianRelationshipOptions; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="soa-guardian-search">
<div class="soa-guardian-search__label">
<mat-icon>manage_accounts</mat-icon>
<span>Search &amp; Link Guardian (existing client)</span>
</div>
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
@if (guardianName) {
<div class="soa-guardian-selected">
<mat-icon class="soa-guardian-selected__icon">verified_user</mat-icon>
<div>
<div class="soa-guardian-selected__name">{{ guardianName }}</div>
<div class="soa-guardian-selected__cif">CIF: {{ guardianCif || 'N/A' }}</div>
</div>
<span class="soa-pill soa-pill--ok">Guardian Linked</span>
</div>
}
</div>
<div class="soa-info-banner soa-info-banner--minor">
<mat-icon>info</mat-icon>
<ul>
<li>Minor below 10 years — Guardian must operate the account.</li>
<li>Minor above 10 years — Can operate independently (bank discretion).</li>
<li>Account must be converted to regular savings on attaining majority.</li>
<li>Fresh KYC of the minor must be done on majority.</li>
</ul>
</div>
</div>
}
<!-- ── SENIOR CITIZEN ────────────────────────────────────────── -->
@if (selectedAccountStatus === 'SENIOR_CITIZEN') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #10b981">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">elderly</mat-icon>
</span>
<div>
<span class="soa-block__title">Senior Citizen Account</span>
<span class="soa-block__hint">Age ≥ 60 years — eligible for preferential banking services</span>
</div>
</div>
<span class="soa-pill soa-pill--senior">Age ≥ 60</span>
</div>
<!-- DOB + Age from client profile -->
<div class="soa-minor-grid" style="margin-bottom: 16px">
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Birth</mat-label>
<input matInput [value]="formatTemplateDate(clientDob)" readonly />
<mat-hint>From client profile</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Age ≥ 60 required for Senior Citizen</mat-hint>
</mat-form-field>
</div>
<div class="soa-sc-benefits">
<div class="soa-sc-benefit">
<mat-icon>priority_high</mat-icon>
<div>
<strong>Priority Service</strong>
<span>Dedicated counters / assisted banking at branches</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>percent</mat-icon>
<div>
<strong>Preferential Interest Rate</strong>
<span>Additional 0.25%–0.50% on deposits (as per product)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>health_and_safety</mat-icon>
<div>
<strong>Doorstep Banking</strong>
<span>Eligible for home banking services (RBI circular)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>gavel</mat-icon>
<div>
<strong>Nomination Mandatory</strong>
<span>RBI mandate — ensure nomination is registered</span>
</div>
</div>
</div>
<div class="soa-info-banner soa-info-banner--senior">
<mat-icon>info</mat-icon>
<ul>
<li>Age proof (DOB document) mandatory at time of account opening.</li>
<li>Nomination registration is compulsory (Form DA-1).</li>
<li>Bank must offer dedicated helpline and priority grievance redressal.</li>
<li>Door-step banking services must be offered (RBI Master Direction 2023).</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL MALE ───────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_MALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #3b82f6">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Male)</span>
<span class="soa-block__hint"
>Select special category if applicable — governs applicable schemes &amp; benefits</span
>
</div>
</div>
<span class="soa-pill" style="background: #dbeafe; color: #1d4ed8; border-color: #bfdbfe">
Special Category
</span>
</div>
<div class="soa-category-grid">
@for (opt of specialCategoryOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value"
[class.soa-cat-tile--male]="true"
(click)="selectedSpecialCategory = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedSpecialCategory === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--male">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).</li>
<li>NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL FEMALE ──────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #ec4899">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person_outline</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Female)</span>
<span class="soa-block__hint"
>Select weaker section category if applicable — governs PMJDY &amp; priority schemes</span
>
</div>
</div>
<span class="soa-pill" style="background: #fce7f3; color: #be185d; border-color: #f9a8d4">
Weaker Section
</span>
</div>
<div class="soa-category-grid">
@for (opt of weakerSectionOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedWeakerSection === opt.value"
[class.soa-cat-tile--female]="true"
(click)="selectedWeakerSection = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedWeakerSection === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--female">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).</li>
<li>PMJDY beneficiary — ₹10,000 overdraft facility &amp; accident insurance cover applicable.</li>
<li>SHG members — group linkage lending &amp; special deposit rates applicable.</li>
</ul>
</div>
</div>
}
</div>
}
</form>
@@ -1,4 +1,4 @@
/**
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
@@ -11,13 +11,17 @@ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorR
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { MatDialog } from '@angular/material/dialog';
/** Custom Services */
import { SavingsService } from 'app/savings/savings.service';
import { ClientsService } from 'app/clients/clients.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
import { MatIcon } from '@angular/material/icon';
import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component';
/**
* Savings Account Details Step
@@ -32,30 +36,27 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
FaIconComponent,
MatStepperNext,
IntroducerSearchComponent,
FormsModule
FormsModule,
MatIcon
]
})
export class SavingsAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private savingsService = inject(SavingsService);
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
private dialog = inject(MatDialog);
/** Savings Account Template */
@Input() savingsAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Product Data */
productData: any;
/** Field Officer Data */
fieldOfficerData: any;
/** For edit savings form */
isFieldOfficerPatched = false;
/** Savings Account Details Form */
savingsAccountDetailsForm: UntypedFormGroup;
savingsProductSelected = false;
@@ -63,22 +64,93 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
introducerName: string | null = null;
introducerCif: string | null = null;
/** Savings Account Template with product data */
@Output() savingsAccountProductTemplate = new EventEmitter();
/** Local copy of product template for displaying classification info */
productTemplate: any = null;
accountTypeOptions: string[] = [];
customerSegmentOptions: string[] = [];
selectedAccountType: string | null = null;
selectedCustomerSegment: string | null = null;
/**
* Sets share account details form.
* @param {FormBuilder} formBuilder Form Builder.
* @param {SavingsService} savingsService Savings Service.
* @param {SettingsService} settingsService Setting service
*/
// ── Client info (auto-fetched) ─────────────────────────────────────────
clientDob: Date | null = null;
clientAge: number | null = null;
clientGender: string | null = null;
primaryClientName = '';
primaryClientCif = '';
primaryClientId: number | null = null;
primaryHolderShare = 0;
// ── Status of Account (RBI) ────────────────────────────────────────────
readonly accountStatusOptions = [
{ value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' },
{ value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' },
{ value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' },
{ value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' },
{ value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' },
{ value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' }
];
selectedAccountStatus = '';
// ── Joint Account ──────────────────────────────────────────────────────
jointHolders: Array<{
clientId: number;
clientName: string;
clientCif: string;
sharePercentage: number;
isMandatory: boolean;
signingAuthority?: string;
role?: string;
}> = [];
modeOfOperation = '';
readonly modeOfOperationOptions = [
{ value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' },
{ value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' },
{ value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' },
{ value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' }
];
// ── Individual Male — Special Category ────────────────────────────────
selectedSpecialCategory = '';
readonly specialCategoryOptions = [
{ value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' },
{ value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' },
{ value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' },
{ value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' },
{ value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' },
{ value: 'NONE', label: 'None / General', icon: 'person' }
];
// ── Individual Female — Weaker Section ────────────────────────────────
selectedWeakerSection = '';
readonly weakerSectionOptions = [
{ value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' },
{ value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' },
{ value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' },
{ value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' },
{ value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' },
{ value: 'NONE', label: 'None / General', icon: 'person_outline' }
];
// ── Minor Account ──────────────────────────────────────────────────────
guardianClientId: number | null = null;
guardianName = '';
guardianCif = '';
guardianRelationship = '';
minorDob: Date | null = null;
readonly guardianRelationshipOptions = [
'Father',
'Mother',
'Legal Guardian',
'Court Appointed Guardian'
];
constructor() {
this.createSavingsAccountDetailsForm();
}
@@ -102,11 +174,83 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
});
}
}
// Auto-detect account status from client DOB + capture primary holder info
const clientId = this.savingsAccountTemplate?.clientId;
if (clientId) {
this.primaryClientId = clientId;
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
this.primaryClientName = client?.displayName ?? client?.fullname ?? '';
this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? '';
if (client?.dateOfBirth) {
const dob = this.parseClientDob(client.dateOfBirth);
if (dob) {
this.clientDob = dob;
this.clientAge = this.calculateAge(dob);
this.clientGender = client.gender?.name?.toLowerCase() ?? null;
this.autoDetectAccountStatus();
}
}
this.cdr.detectChanges();
});
}
}
/** Parse DOB from backend — handles array [yyyy, mm, dd] or string */
private parseClientDob(dob: any): Date | null {
if (!dob) return null;
if (Array.isArray(dob) && dob.length >= 3) {
return new Date(dob[0], dob[1] - 1, dob[2]);
}
const d = new Date(dob);
return isNaN(d.getTime()) ? null : d;
}
/** Calculate full years from a date to today */
private calculateAge(dob: Date): number {
const today = this.settingsService.businessDate ?? new Date();
let age = today.getFullYear() - dob.getFullYear();
const monthDiff = today.getMonth() - dob.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
age--;
}
return age;
}
isTileDisabled(value: string): boolean {
if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18;
if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60;
return false;
}
getTileTooltip(value: string): string {
if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) {
return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`;
}
if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) {
return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`;
}
if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age';
if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age';
return '';
}
/** Auto-set status tile based on age; pre-fill minorDob if minor */
private autoDetectAccountStatus(): void {
if (this.clientAge === null) return;
if (this.clientAge < 18) {
this.selectedAccountStatus = 'MINOR';
this.minorDob = this.clientDob;
} else if (this.clientAge >= 60) {
this.selectedAccountStatus = 'SENIOR_CITIZEN';
} else if (this.clientGender === 'female') {
this.selectedAccountStatus = 'INDIVIDUAL_FEMALE';
} else {
this.selectedAccountStatus = 'INDIVIDUAL_MALE';
}
}
/**
* Creates savings account details form.
*/
createSavingsAccountDetailsForm() {
this.savingsAccountDetailsForm = this.formBuilder.group({
productId: [
@@ -122,9 +266,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
});
}
/**
* Fetches savings account product template on productId value changes
*/
buildDependencies() {
const entityId = this.savingsAccountTemplate.groupId || this.savingsAccountTemplate.clientId;
this.savingsAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
@@ -150,10 +291,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
});
}
/**
* Formats template dates into the current CBS date format.
* Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects.
*/
private extractOptions(options: any[], fallback?: string): string[] {
if (Array.isArray(options) && options.length > 0) {
return options.map((o: any) => o.value ?? o.name ?? o);
@@ -162,12 +299,8 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
}
formatTemplateDate(value: any): string {
if (!value) {
return '';
}
if (!value) return '';
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
} else if (Array.isArray(value) && value.length >= 3) {
@@ -175,30 +308,145 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
} else {
dateValue = new Date(value);
}
if (!dateValue || Number.isNaN(dateValue.getTime())) {
return Array.isArray(value) ? value.join('/') : String(value);
}
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
/**
* Returns savings account form value.
*/
onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.introducerId = event?.clientId ?? null;
this.introducerName = event?.displayName ?? null;
this.introducerCif = event?.cifNumber ?? null;
}
onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.guardianClientId = event?.clientId ?? null;
this.guardianName = event?.displayName ?? '';
this.guardianCif = event?.cifNumber ?? '';
}
// ── Joint Account helpers ──────────────────────────────────────────────
openAddJointHolderDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
panelClass: 'arc-dialog-panel',
disableClose: true
});
ref.afterClosed().subscribe((result) => {
if (result) {
const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif);
if (duplicate) {
alert(`Account holder with CIF ${result.clientCif} is already added.`);
return;
}
this.jointHolders = [
...this.jointHolders,
{ ...result, isMandatory: false }];
this.autoSetModeOfOperation();
}
});
}
removeJointHolder(index: number): void {
this.jointHolders = this.jointHolders.filter((_, i) => i !== index);
this.autoSetModeOfOperation();
}
toggleJointHolderMandatory(holder: any): void {
holder.isMandatory = !holder.isMandatory;
this.autoSetModeOfOperation();
}
autoSetModeOfOperation(): void {
const mandatory = this.jointHolders.filter((h) => h.isMandatory).length;
if (mandatory === 0) {
this.modeOfOperation = 'EITHER_OR_SURVIVOR';
} else if (mandatory === 1) {
this.modeOfOperation = 'FORMER_OR_SURVIVOR';
} else {
this.modeOfOperation = 'JOINTLY';
}
}
get totalJointSharePercentage(): number {
return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0);
}
get mandatoryJointCount(): number {
return this.jointHolders.filter((h) => h.isMandatory).length;
}
get selectedModeLabel(): string {
return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? '';
}
// ── Minor Account helpers ──────────────────────────────────────────────
get majorityDate(): string {
const dob = this.minorDob ?? this.clientDob;
if (!dob) return '';
const d = new Date(dob);
d.setFullYear(d.getFullYear() + 18);
return this.dateUtils.formatDate(d, 'M/d/yyyy') || '';
}
get savingsAccountDetails() {
const value = this.savingsAccountDetailsForm.getRawValue();
// Product Classification — user-selected values
if (this.selectedAccountType) value.accountType = this.selectedAccountType;
if (this.selectedCustomerSegment) value.customerSegment = this.selectedCustomerSegment;
if (this.introducerId) {
value.introducerId = this.introducerId;
value.introducerName = this.introducerName;
value.introducerCif = this.introducerCif;
value.introducerCifNumber = this.introducerCif;
}
if (this.selectedAccountStatus) {
value.accountStatus = this.selectedAccountStatus;
}
if (this.selectedAccountStatus === 'JOINT') {
value.modeOfOperation = this.modeOfOperation || null;
value.jointHolders = [
{
clientId: this.primaryClientId,
displayName: this.primaryClientName,
cifNumber: this.primaryClientCif,
sharePercentage: this.primaryHolderShare,
isMandatory: true,
isPrimary: true
},
...this.jointHolders.map((h) => ({
clientId: h.clientId,
displayName: h.clientName,
cifNumber: h.clientCif,
sharePercentage: h.sharePercentage,
isPrimary: false,
isMandatory: h.isMandatory,
signingAuthority: h.signingAuthority || null,
role: h.role || null
}))
];
}
if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) {
value.specialCategory = this.selectedSpecialCategory;
}
if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) {
value.weakerSection = this.selectedWeakerSection;
}
if (this.selectedAccountStatus === 'MINOR') {
value.guardianClientId = this.guardianClientId;
value.guardianName = this.guardianName;
value.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship;
value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null;
}
return value;
}
}
@@ -63,16 +63,16 @@
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (savingsAccountProductTemplate?.accountType) {
@if (savingsAccount?.accountType || savingsAccountProductTemplate?.accountType) {
<div class="preview-field">
<span class="preview-label">Account Type</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.accountType }}</span>
<span class="preview-value">{{ savingsAccount?.accountType || savingsAccountProductTemplate?.accountType }}</span>
</div>
}
@if (savingsAccountProductTemplate?.customerSegment) {
@if (savingsAccount?.customerSegment || savingsAccountProductTemplate?.customerSegment) {
<div class="preview-field">
<span class="preview-label">Customer Segment</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.customerSegment }}</span>
<span class="preview-value">{{ savingsAccount?.customerSegment || savingsAccountProductTemplate?.customerSegment }}</span>
</div>
}
@if (savingsAccountProductTemplate?.productCode) {
@@ -135,10 +135,10 @@
<span class="preview-label">Name</span>
<span class="preview-value">{{ savingsAccount?.introducerName || '—' }}</span>
</div>
@if (savingsAccount?.introducerCif) {
@if (savingsAccount?.introducerCifNumber) {
<div class="preview-field">
<span class="preview-label">CIF No</span>
<span class="preview-value preview-mono">{{ savingsAccount.introducerCif }}</span>
<span class="preview-value preview-mono">{{ savingsAccount.introducerCifNumber }}</span>
</div>
}
<div class="preview-field">
@@ -150,7 +150,155 @@
</div>
}
<!-- ══ 4. Currency & Account Flags ══════════════════════════════ -->
<!-- ══ 4. Status of Account ════════════════════════════════════ -->
@if (savingsAccount?.accountStatus) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-id-card preview-section__icon"></i>
<span class="preview-section__title">Status of Account</span>
<span class="preview-section__badge">RBI Guidelines</span>
</div>
<div class="preview-body">
<!-- Status Tile -->
<div class="preview-soa-status-row">
<span class="preview-soa-tile">
@switch (savingsAccount.accountStatus) {
@case ('INDIVIDUAL_MALE') { <i class="fas fa-male"></i> Individual (Male) }
@case ('INDIVIDUAL_FEMALE') { <i class="fas fa-female"></i> Individual (Female) }
@case ('SINGLE') { <i class="fas fa-user-circle"></i> Single Account }
@case ('JOINT') { <i class="fas fa-users"></i> Joint Account }
@case ('MINOR') { <i class="fas fa-child"></i> Minor Account }
@case ('SENIOR_CITIZEN') { <i class="fas fa-user-clock"></i> Senior Citizen }
@default { {{ savingsAccount.accountStatus }} }
}
</span>
</div>
<!-- JOINT: Mode of Operation + Holders Table -->
@if (savingsAccount.accountStatus === 'JOINT') {
<div class="preview-grid preview-grid--three" style="margin-top:12px">
<div class="preview-field">
<span class="preview-label">Mode of Operation</span>
<span class="preview-value">
@switch (savingsAccount.modeOfOperation) {
@case ('EITHER_OR_SURVIVOR') { Either or Survivor }
@case ('FORMER_OR_SURVIVOR') { Former or Survivor }
@case ('JOINTLY') { Jointly }
@case ('ANYONE_OR_SURVIVOR') { Anyone or Survivor }
@default { {{ savingsAccount.modeOfOperation || '—' }} }
}
</span>
</div>
</div>
@if (savingsAccount.jointHolders?.length) {
<div class="preview-soa-table-wrap">
<table class="preview-soa-tbl">
<thead>
<tr>
<th class="preview-soa-th">Sr.</th>
<th class="preview-soa-th">Holder Name</th>
<th class="preview-soa-th">CIF No.</th>
<th class="preview-soa-th">Ownership %</th>
<th class="preview-soa-th">Signing</th>
<th class="preview-soa-th">Role</th>
</tr>
</thead>
<tbody>
@for (holder of savingsAccount.jointHolders; track holder.clientId; let i = $index) {
<tr [class.preview-soa-tr--primary]="holder.isPrimary">
<td class="preview-soa-td">{{ i + 1 }}</td>
<td class="preview-soa-td">
{{ holder.displayName || '—' }}
@if (holder.isPrimary) { <span class="preview-soa-primary-tag">Primary</span> }
</td>
<td class="preview-soa-td preview-mono">{{ holder.cifNumber || '—' }}</td>
<td class="preview-soa-td">{{ holder.sharePercentage ?? '—' }}%</td>
<td class="preview-soa-td">{{ holder.isMandatory ? 'Mandatory' : 'Optional' }}</td>
<td class="preview-soa-td">{{ holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder' }}</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- INDIVIDUAL_MALE: Special Category -->
@if (savingsAccount.accountStatus === 'INDIVIDUAL_MALE' && savingsAccount.specialCategory) {
<div class="preview-grid preview-grid--three" style="margin-top:12px">
<div class="preview-field">
<span class="preview-label">Special Category</span>
<span class="preview-value">
@switch (savingsAccount.specialCategory) {
@case ('DEFENCE') { Defence Personnel }
@case ('EX_SERVICEMAN') { Ex-Serviceman }
@case ('GOVT_EMPLOYEE') { Government Employee }
@case ('PHYSICALLY_CHALLENGED') { Physically Challenged }
@case ('NRI') { NRI (Non-Resident Indian) }
@case ('NONE') { None / General }
@default { {{ savingsAccount.specialCategory }} }
}
</span>
</div>
</div>
}
<!-- INDIVIDUAL_FEMALE: Weaker Section -->
@if (savingsAccount.accountStatus === 'INDIVIDUAL_FEMALE' && savingsAccount.weakerSection) {
<div class="preview-grid preview-grid--three" style="margin-top:12px">
<div class="preview-field">
<span class="preview-label">Weaker Section</span>
<span class="preview-value">
@switch (savingsAccount.weakerSection) {
@case ('BPL') { BPL (Below Poverty Line) }
@case ('SC_ST') { SC / ST Category }
@case ('MINORITY') { Minority Community }
@case ('SHG') { Self Help Group Member }
@case ('PMJDY') { PMJDY Beneficiary }
@case ('NONE') { None / General }
@default { {{ savingsAccount.weakerSection }} }
}
</span>
</div>
</div>
}
<!-- MINOR: Guardian Details -->
@if (savingsAccount.accountStatus === 'MINOR') {
<div class="preview-grid preview-grid--three" style="margin-top:12px">
@if (savingsAccount.guardianName) {
<div class="preview-field">
<span class="preview-label">Guardian Name</span>
<span class="preview-value">{{ savingsAccount.guardianName }}</span>
</div>
}
@if (savingsAccount.guardianCifNumber) {
<div class="preview-field">
<span class="preview-label">Guardian CIF</span>
<span class="preview-value preview-mono">{{ savingsAccount.guardianCifNumber }}</span>
</div>
}
@if (savingsAccount.guardianRelationship) {
<div class="preview-field">
<span class="preview-label">Relationship</span>
<span class="preview-value">{{ savingsAccount.guardianRelationship }}</span>
</div>
}
@if (savingsAccount.minorDateOfBirth) {
<div class="preview-field">
<span class="preview-label">Minor DOB</span>
<span class="preview-value">{{ savingsAccount.minorDateOfBirth }}</span>
</div>
}
</div>
}
</div>
</div>
}
<!-- ══ 5. Currency & Account Flags ══════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-coins preview-section__icon"></i>
@@ -323,3 +323,75 @@
.margin-t {
margin-top: 1em;
}
// ── Status of Account preview ─────────────────────────────────────────────
.preview-soa-status-row {
display: flex;
align-items: center;
gap: 10px;
}
.preview-soa-tile {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
border-radius: 999px;
font-size: 14px;
font-weight: 700;
background: #eff6ff;
color: #1d4ed8;
border: 2px solid #bfdbfe;
i { font-size: 16px; }
}
.preview-soa-table-wrap {
margin-top: 14px;
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.preview-soa-tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.preview-soa-th {
background: #f8fafc;
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: #475569;
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
.preview-soa-td {
padding: 10px 12px;
border-bottom: 1px solid #f1f5f9;
color: #0f172a;
vertical-align: middle;
}
.preview-soa-tr--primary {
background: #f0f9ff;
.preview-soa-td { font-weight: 600; }
}
.preview-soa-primary-tag {
display: inline-block;
margin-left: 6px;
padding: 1px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
vertical-align: middle;
}
+5
View File
@@ -64,6 +64,11 @@ export class SavingsService {
return this.http.get(`/savingsaccounts/${accountId}`, { params: httpParams });
}
getClientSavingsAccounts(clientId: string): Observable<any> {
const httpParams = new HttpParams().set('clientId', clientId);
return this.http.get('/savingsaccounts', { params: httpParams });
}
/**
* @param accountId Savings Account Id of account to get data for.
* @returns {Observable<any>} Savings account and template.
@@ -84,7 +84,7 @@ export class IntroducerSearchComponent {
this.introducerChange.emit({
clientId: client.id,
displayName: client.displayName,
cifNumber: client.cifNumber || client.accountNumber || ''
cifNumber: client.cifNumber || client.cifNo || ''
});
}
@@ -183,7 +183,7 @@ export class NomineeFormComponent implements OnInit {
mode: 'linked',
clientId: client.id,
displayName: client.displayName,
cifNumber: client.accountNo || client.cifNumber || ''
cifNumber: client.cifNumber || client.cifNo || ''
});
}