feat: merge updates - current account, savings, recurring deposits and shared components

This commit is contained in:
Nitesh Yadav
2026-04-29 15:20:31 +05:30
25 changed files with 6816 additions and 176 deletions
@@ -74,12 +74,20 @@
<div class="rad-grid rad-grid--classification">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Account Type</mat-label>
<input matInput [value]="productTemplate.accountType || ''" readonly />
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }">
@for (opt of accountTypeOptions; track opt) {
<mat-option [value]="opt">{{ opt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Customer Segment</mat-label>
<input matInput [value]="productTemplate.customerSegment || ''" readonly />
<mat-select [(ngModel)]="selectedCustomerSegment" [ngModelOptions]="{ standalone: true }">
@for (opt of customerSegmentOptions; track opt) {
<mat-option [value]="opt">{{ opt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
@@ -107,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>
@@ -121,8 +124,704 @@
</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>
@@ -8,17 +8,20 @@
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
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,7 +36,8 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
MatStepperPrevious,
FaIconComponent,
MatStepperNext,
IntroducerSearchComponent
FormsModule,
MatIcon
]
})
export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
@@ -42,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;
@@ -60,15 +66,164 @@ 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();
/** 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 ──────────────────────────────────────────────────────
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'
];
// ── 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();
@@ -80,7 +235,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:
@@ -89,12 +243,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';
}
}
/**
@@ -127,9 +353,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
);
@@ -140,10 +364,16 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
}
}
this.productTemplate = response;
this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType);
this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment);
this.selectedAccountType = response.accountType ?? null;
this.selectedCustomerSegment = response.customerSegment ?? null;
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,
@@ -162,24 +392,20 @@ 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.
*/
formatTemplateDate(value: any): string {
if (!value) {
return '';
private extractOptions(options: any[], fallback?: string): string[] {
if (Array.isArray(options) && options.length > 0) {
return options.map((o: any) => o.value ?? o.name ?? o);
}
return fallback ? [fallback] : [];
}
formatTemplateDate(value: any): string {
if (!value) return '';
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
} else if (Array.isArray(value) && value.length >= 3) {
@@ -187,30 +413,139 @@ 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,
signingAuthority: 'Mandatory',
role: 'Primary Account Holder'
},
...this.jointHolders.map((h) => ({
clientId: h.clientId,
clientName: h.clientName,
clientCif: h.clientCif,
sharePercentage: h.sharePercentage,
isMandatory: h.isMandatory,
isPrimary: false,
signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'),
role: h.role || 'Co-Holder'
}))
];
}
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"