update fd account

This commit is contained in:
Nitesh Yadav
2026-05-07 11:17:49 +05:30
parent b0cffd3845
commit a12ba20de4
15 changed files with 5727 additions and 774 deletions
@@ -45,38 +45,15 @@
</mifosx-fixed-deposit-account-details-step>
</mat-step>
<!-- Step 2: Currency -->
@if (fixedDepositsAccountProductTemplate) {
<!-- Step 2: Terms -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<ng-template matStepLabel>Currency</ng-template>
<mifosx-fixed-deposit-account-terms-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
<mifosx-fixed-deposit-account-currency-step
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-terms-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 3: Settings -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-fixed-deposit-account-settings-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-settings-step>
</mifosx-fixed-deposit-account-currency-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
@@ -97,6 +74,7 @@
<mifosx-fixed-deposit-account-interest-rate-chart-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
(tenureSelected)="onTenureSelected($event)"
>
</mifosx-fixed-deposit-account-interest-rate-chart-step>
@@ -112,29 +90,6 @@
</div>
</mat-step>
<!-- Step 5: Charges -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-fixed-deposit-account-charges-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[fixedDepositAccountFormValid]="fixedDepositAccountFormValid"
>
</mifosx-fixed-deposit-account-charges-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 6: Preview -->
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
@@ -142,6 +97,7 @@
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[fixedDepositAccountData]="fixedDepositAccount"
[selectedTenure]="selectedTenure"
(submitEvent)="submit()"
>
</mifosx-fixed-deposit-account-preview-step>
@@ -16,9 +16,7 @@ import { SettingsService } from 'app/settings/settings.service';
/** Custom Components */
import { FixedDepositAccountDetailsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component';
import { FixedDepositAccountTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-terms-step/fixed-deposit-account-terms-step.component';
import { FixedDepositAccountSettingsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-settings-step/fixed-deposit-account-settings-step.component';
import { FixedDepositAccountChargesStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-charges-step/fixed-deposit-account-charges-step.component';
import { FixedDepositAccountCurrencyStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component';
import { Dates } from 'app/core/utils/dates';
import {
MatStepper,
@@ -50,10 +48,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatStep,
MatStepLabel,
FixedDepositAccountDetailsStepComponent,
FixedDepositAccountTermsStepComponent,
FixedDepositAccountSettingsStepComponent,
FixedDepositAccountInterestRateChartStepComponent,
FixedDepositAccountChargesStepComponent,
FixedDepositAccountCurrencyStepComponent,
FixedDepositAccountPreviewStepComponent
]
})
@@ -66,17 +62,15 @@ export class CreateFixedDepositAccountComponent {
@ViewChild(FixedDepositAccountDetailsStepComponent, { static: true })
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
@ViewChild(FixedDepositAccountTermsStepComponent)
fixedDepositAccountTermsStep: FixedDepositAccountTermsStepComponent;
@ViewChild(FixedDepositAccountSettingsStepComponent)
fixedDepositAccountSettingsStep: FixedDepositAccountSettingsStepComponent;
@ViewChild(FixedDepositAccountChargesStepComponent)
fixedDepositAccountChargesStep: FixedDepositAccountChargesStepComponent;
@ViewChild(FixedDepositAccountCurrencyStepComponent)
fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent;
/** Fixed Deposits Account Template */
fixedDepositsAccountTemplate: any;
/** Fixed Deposit Account Product Template */
fixedDepositsAccountProductTemplate: any;
/** Tenure selected from the interest rate chart step */
selectedTenure: { depositPeriod: number; depositPeriodFrequencyId: number } | null = null;
/**
* Fetches FD account template from `resolve`
@@ -100,36 +94,22 @@ export class CreateFixedDepositAccountComponent {
this.fixedDepositsAccountProductTemplate = $event;
}
onTenureSelected($event: { depositPeriod: number; depositPeriodFrequencyId: number }) {
this.selectedTenure = $event;
}
/**
* Retrieves Fixed Deposit Account Details Form Data
*/
get fixedDepositAccountDetailsForm() {
return this.fixedDepositsAccountDetailsStep.fixedDepositAccountDetailsForm;
}
/**
* Retrieves Fixed Deposit Account Terms Form Data
*/
get fixedDepositAccountTermsForm() {
return this.fixedDepositAccountTermsStep.fixedDepositAccountTermsForm;
}
/**
* Retrieves Fixed Deposit Account Settings Form Data
*/
get fixedDepositAccountSettingsForm() {
return this.fixedDepositAccountSettingsStep.fixedDepositAccountSettingsForm;
return this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetailsForm;
}
/**
* Checks stepper validity.
*/
get fixedDepositAccountFormValid() {
return (
this.fixedDepositAccountDetailsForm.valid &&
this.fixedDepositAccountTermsForm.valid &&
this.fixedDepositAccountSettingsForm.valid
);
return !!this.fixedDepositAccountDetailsForm?.valid;
}
/**
@@ -137,10 +117,8 @@ export class CreateFixedDepositAccountComponent {
*/
get fixedDepositAccount() {
return {
...this.fixedDepositsAccountDetailsStep.fixedDepositAccountDetails,
...this.fixedDepositAccountTermsStep.fixedDepositAccountTerms,
...this.fixedDepositAccountSettingsStep.fixedDepositAccountSettings,
...this.fixedDepositAccountChargesStep.fixedDepositAccountCharges
...this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails,
...this.fixedDepositAccountCurrencyStep?.currencyStepData
};
}
@@ -153,6 +131,7 @@ export class CreateFixedDepositAccountComponent {
const monthDayFormat = 'dd MMMM';
const fixedDepositAccount = {
...this.fixedDepositAccount,
...(this.selectedTenure ?? {}),
clientId: this.fixedDepositsAccountTemplate.clientId,
charges: this.fixedDepositAccount.charges.map((charge: any) => ({
chargeId: charge.id,
@@ -163,7 +142,7 @@ export class CreateFixedDepositAccountComponent {
feeInterval: charge.feeInterval
})),
submittedOnDate: this.dateUtils.formatDate(this.fixedDepositAccount.submittedOnDate, dateFormat),
charts: [{ chartSlabs: this.fixedDepositsAccountProductTemplate.accountChart.chartSlabs }],
charts: [{ chartSlabs: this.fixedDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [] }],
dateFormat,
monthDayFormat,
locale
@@ -0,0 +1,66 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="facc-section">
<div class="facc-section__header">
<i class="fas fa-coins"></i>
<span>Currency &amp; Account Status Configuration</span>
<span class="facc-badge facc-badge--from">From Product</span>
</div>
<div class="facc-body">
<div class="facc-grid">
<mat-form-field appearance="outline" class="facc-field">
<mat-label>Currency</mat-label>
<input matInput [value]="fixedDepositsAccountProductTemplate?.currency?.name || ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="facc-field">
<mat-label>Decimal Places</mat-label>
<input matInput [value]="fixedDepositsAccountProductTemplate?.currency?.decimalPlaces ?? ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
</div>
<div class="facc-alert">
<div class="facc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-slide-toggle [(ngModel)]="autoDebitFacility" color="primary"></mat-slide-toggle>
</div>
<div class="facc-alert">
<div class="facc-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<span class="facc-yn" [class.facc-yn--yes]="fixedDepositsAccountProductTemplate?.dormancyAlert">
{{ fixedDepositsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }}
</span>
</div>
@if (fixedDepositsAccountProductTemplate?.dormancyAlert) {
<mifosx-dormancy-alert-panel
[productTemplate]="fixedDepositsAccountProductTemplate"
></mifosx-dormancy-alert-panel>
}
<div class="facc-alert">
<div class="facc-alert__content">
<h4>Nomination Facility</h4>
<p>Allow account holders to designate individuals to receive funds in the event of account holder's death.</p>
</div>
<span class="facc-yn" [class.facc-yn--yes]="fixedDepositsAccountProductTemplate?.nominationFacility">
{{ fixedDepositsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
</span>
</div>
@if (fixedDepositsAccountProductTemplate?.nominationFacility) {
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
}
</div>
</div>
@@ -0,0 +1,124 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.facc-section {
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #fff;
overflow: hidden;
margin-bottom: 1.5rem;
&__header {
background-color: #f8fafc;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 0.75rem;
i {
color: #1976d2;
font-size: 1rem;
}
span {
font-weight: 600;
color: #333;
font-size: 0.9rem;
}
}
}
.facc-body {
padding: 1.25rem;
}
.facc-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;
margin-bottom: 1rem;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.facc-field {
width: 100%;
}
.facc-alert {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 1rem;
background-color: #fff;
gap: 1.5rem;
&--editable {
border-color: #bfdbfe;
background-color: #eff6ff;
}
&__content {
flex: 1;
h4 {
margin: 0 0 0.25rem;
font-size: 0.95rem;
font-weight: 600;
color: #1e293b;
}
p {
margin: 0;
font-size: 0.8rem;
color: #64748b;
line-height: 1.4;
}
}
}
.facc-badge {
padding: 0.2rem 0.6rem;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
&--from {
background-color: #dbeafe;
color: #1e40af;
}
}
.facc-yn {
padding: 0.25rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
background-color: #fee2e2;
color: #991b1b;
min-width: 60px;
text-align: center;
&--yes {
background-color: #dcfce7;
color: #166534;
}
}
:host ::ng-deep {
.mat-mdc-form-field-subscript-wrapper {
font-size: 0.75rem;
}
}
@@ -0,0 +1,55 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component';
import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'mifosx-fixed-deposit-account-currency-step',
templateUrl: './fixed-deposit-account-currency-step.component.html',
styleUrls: ['./fixed-deposit-account-currency-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
NomineeFormComponent,
DormancyAlertPanelComponent,
MatSlideToggle,
FormsModule
]
})
export class FixedDepositAccountCurrencyStepComponent implements OnChanges {
@Input() fixedDepositsAccountProductTemplate: any;
nomineeData: any = null;
autoDebitFacility = false;
// Editable feature toggles (pre-filled from product, overridable by user)
ngOnChanges(changes: SimpleChanges): void {
if (changes['fixedDepositsAccountProductTemplate']?.currentValue) {
const pt = changes['fixedDepositsAccountProductTemplate'].currentValue;
this.autoDebitFacility = !!pt.autoDebitFacility;
}
}
onNomineeChange(event: any): void {
this.nomineeData = event;
}
/** Called by parent to collect currency step values */
get currencyStepData() {
return {
autoDebitFacility: this.autoDebitFacility,
...(this.nomineeData ? { nominee: this.nomineeData } : {})
};
}
}
@@ -7,67 +7,883 @@
-->
<form [formGroup]="fixedDepositAccountDetailsForm">
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<mat-select formControlName="productId" required>
@for (product of productData; track product) {
<mat-option [value]="product.id">
{{ product.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (isProductSelected) {
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<!-- ══ Account Details ══════════════════════════════════════════ -->
<div class="rad-section">
<div class="rad-section__header">
<i class="fas fa-info-circle"></i>
<h3>Account Details</h3>
</div>
<div class="rad-grid">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<mat-select formControlName="productId" required>
@for (product of productData; track product) {
<mat-option [value]="product.id">{{ product.name }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Submission Date' | translate }} {{ 'labels.commons.is' | translate }}
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
@if (isProductSelected) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId">
@for (officer of fieldOfficerData; track officer) {
<mat-option [value]="officer.id">
{{ officer.displayName }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (isProductSelected) {
<mat-form-field appearance="outline" class="rad-field" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (isProductSelected) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.External id' | translate }}</mat-label>
<input
matInput
formControlName="externalId"
matTooltip="{{ 'tooltips.Provides an external id' | translate }}"
/>
</mat-form-field>
}
<mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId">
@for (officer of fieldOfficerData; track officer) {
<mat-option [value]="officer.id">{{ officer.displayName }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.External id' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
}
</div>
</div>
@if (isProductSelected && productTemplate) {
<!-- ══ Product Classification (read-only) ══════════════════════ -->
<div class="rad-section rad-section--mt">
<div class="rad-section__header">
<i class="fas fa-tags"></i>
<h3>Product Classification</h3>
<span class="rad-badge">From Product</span>
</div>
<div class="rad-grid rad-grid--classification">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Account Type</mat-label>
<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>
@if (productTemplate?.fdType) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>FD Type</mat-label>
<input matInput [value]="getFdTypeLabel(productTemplate.fdType)" readonly />
</mat-form-field>
}
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Customer Segment</mat-label>
<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">
<mat-label>Account No</mat-label>
<input
matInput
[value]="productTemplate?.previewAccountNo || fixedDepositsAccountTemplate?.accountNo || 'Auto-generated'"
readonly
/>
</mat-form-field>
@if (productTemplate.effectiveDate) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Effective Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.effectiveDate)" readonly />
</mat-form-field>
}
@if (productTemplate.endDate) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>End Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.endDate)" readonly />
</mat-form-field>
}
<div class="rad-field rad-info-row">
<span class="rad-info-label">Deposit Account Link</span>
<span class="rad-badge" [class.rad-badge--yes]="productTemplate.shareLinkingEnabled">
{{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }}
</span>
</div>
</div>
</div>
<!-- ══ Currency (read-only) ════════════════════════════════════ -->
<div class="rad-section rad-section--mt">
<div class="rad-section__header">
<i class="fas fa-coins"></i>
<h3>Currency</h3>
<span class="rad-badge">From Product</span>
</div>
<div class="rad-grid rad-grid--classification">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<input matInput [value]="currencyName" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<input matInput [value]="decimalPlaces" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.Currency In Multiples Of' | translate }}</mat-label>
<input matInput [value]="inMultiplesOf" readonly />
</mat-form-field>
</div>
</div>
<!-- ══ 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 FD</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)="selectAccountStatus(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 FD 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)="selectSpecialCategory(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)="selectWeakerSection(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>Eligible for PMJDY overdraft facilities (subject to rules).</li>
<li>Financial literacy & priority processing for women applicants.</li>
</ul>
</div>
</div>
}
<!-- ── SINGLE ACCOUNT ────────────────────────────────────────── -->
@if (selectedAccountStatus === 'SINGLE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #0891b2">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">account_circle</mat-icon>
</span>
<div>
<span class="soa-block__title">Single Account</span>
<span class="soa-block__hint">Standard individual account operated by a single person</span>
</div>
</div>
<span class="soa-pill" style="background: #ecfeff; color: #0e7490; border-color: #cffafe"
>Single Holder</span
>
</div>
<div class="soa-info-banner soa-info-banner--single">
<mat-icon>info</mat-icon>
<ul>
<li>Only one primary holder allowed.</li>
<li>Nomination is highly recommended.</li>
<li>Full independent operation of account.</li>
<li>Convertible to joint account later if required.</li>
</ul>
</div>
</div>
}
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
@@ -7,16 +7,21 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
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 { FixedDepositsService } from '../../fixed-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 { 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';
/**
* Fixed Deposits Account Details Step
@@ -30,13 +35,19 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
MatStepperNext,
FormsModule,
MatIcon
]
})
export class FixedDepositAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
private clientsService = inject(ClientsService);
private dialog = inject(MatDialog);
/** Fixed Deposits Account Template */
@Input() fixedDepositsAccountTemplate: any;
@@ -49,22 +60,194 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
productData: any;
/** Field Officer Data */
fieldOfficerData: any;
/** For edit savings form */
/** For edit fixed deposits form */
isFieldOfficerPatched = false;
/** Fixed Deposits Account Details Form */
fixedDepositAccountDetailsForm: UntypedFormGroup;
fixedDepositAccountDetailsForm!: UntypedFormGroup;
isProductSelected = false;
/** Fixed Deposits Account Template with product data */
@Output() fixedDepositsAccountProductTemplate = new EventEmitter();
/**
* Sets fixed deposits account details form.
* @param {FormBuilder} formBuilder Form Builder.
* @param {FixedDepositsService} fixedDepositsService Fixed Deposits Service.
* @param {SettingsService} settingsService Settings Service
*/
/** Local copy of product template for displaying classification info */
productTemplate: any = null;
accountTypeOptions: string[] = [];
readonly fdTypeLabels: Record<string, string> = {
SINGLE: 'FD Single (Days < 1 Year)',
DOUBLE: 'FD Double (≥ 1 Year)',
SPECIAL: 'FD Special (Monthly Interest Payout)'
};
getFdTypeLabel(fdType: string): string {
return this.fdTypeLabels[fdType] || fdType;
}
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;
currencyFlags: any = null;
get currencyName(): string {
return this.productTemplate?.currency?.name || '';
}
get decimalPlaces(): number {
return this.productTemplate?.currency?.decimalPlaces ?? 0;
}
get inMultiplesOf(): number {
return this.productTemplate?.currency?.inMultiplesOf ?? 0;
}
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.fixedDepositsAccountTemplate?.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.createFixedDepositsAccountDetailsForm();
}
@@ -78,12 +261,109 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
this.fixedDepositAccountDetailsForm.patchValue({
productId: this.fixedDepositsAccountTemplate.depositProductId,
submittedOnDate:
this.fixedDepositsAccountTemplate.timeline.submittedOnDate &&
this.fixedDepositsAccountTemplate.timeline?.submittedOnDate &&
new Date(this.fixedDepositsAccountTemplate.timeline.submittedOnDate),
externalId: this.fixedDepositsAccountTemplate.externalId || ''
externalId: this.fixedDepositsAccountTemplate.externalId
});
} else {
this.fixedDepositAccountDetailsForm.patchValue({
submittedOnDate: this.settingsService.businessDate
});
}
}
// Auto-detect account status from client DOB + capture primary holder info
const clientId = this.fixedDepositsAccountTemplate?.clientId || this.fixedDepositsAccountTemplate?.id;
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;
}
/** Handle Account Status selection */
selectAccountStatus(status: string): void {
if (!this.isTileDisabled(status)) {
this.selectedAccountStatus = status;
this.cdr.detectChanges();
}
}
/** Handle Special Category selection */
selectSpecialCategory(category: string): void {
this.selectedSpecialCategory = category;
this.cdr.detectChanges();
}
/** Handle Weaker Section selection */
selectWeakerSection(section: string): void {
this.selectedWeakerSection = section;
this.cdr.detectChanges();
}
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';
}
}
/**
@@ -108,34 +388,209 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
* Fetches fixed deposits account product template on productId value changes
*/
buildDependencies() {
const clientId = this.fixedDepositsAccountTemplate.clientId;
this.fixedDepositAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
if (!this.fixedDepositsAccountTemplate) return;
const clientId = this.fixedDepositsAccountTemplate.clientId || this.fixedDepositsAccountTemplate.id;
this.fixedDepositAccountDetailsForm.get('productId')!.valueChanges.subscribe((productId: string) => {
this.fixedDepositsService.getFixedDepositsAccountTemplate(clientId, productId).subscribe((response: any) => {
if (!response.currency) {
const selectedProduct = this.fixedDepositsAccountTemplate.productOptions?.find(
(product: any) => product.id == productId
);
if (selectedProduct?.currency) {
response.currency = selectedProduct.currency;
} else if (this.fixedDepositsAccountTemplate.currency) {
response.currency = this.fixedDepositsAccountTemplate.currency;
}
}
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.fixedDepositsAccountProductTemplate.emit(response);
this.isProductSelected = true;
this.fieldOfficerData = response.fieldOfficerOptions;
this.isProductSelected = true;
if (response.shareLinkingEnabled) {
this.loadClientAccounts();
}
this.cdr.detectChanges();
if (!this.isFieldOfficerPatched && this.fixedDepositsAccountTemplate.fieldOfficerId) {
this.fixedDepositAccountDetailsForm
.get('fieldOfficerId')
.get('fieldOfficerId')!
.patchValue(this.fixedDepositsAccountTemplate.fieldOfficerId);
this.isFieldOfficerPatched = true;
} else {
this.fixedDepositAccountDetailsForm.get('fieldOfficerId').patchValue('');
this.fixedDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue('');
}
});
});
}
hasValue(val: any): boolean {
return val !== null && val !== undefined && val !== '';
}
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) {
dateValue = new Date(value[0], value[1] - 1, value[2]);
} 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 ?? '';
this.cdr.detectChanges();
}
// ── 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();
this.cdr.detectChanges();
}
});
}
removeJointHolder(index: number): void {
this.jointHolders = this.jointHolders.filter((_, i) => i !== index);
this.autoSetModeOfOperation();
this.cdr.detectChanges();
}
toggleJointHolderMandatory(holder: any): void {
holder.isMandatory = !holder.isMandatory;
this.autoSetModeOfOperation();
this.cdr.detectChanges();
}
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 fixed deposits account details form value.
*/
get fixedDepositAccountDetails() {
const fixedDepositAccountDetails = this.fixedDepositAccountDetailsForm.value;
for (const key in fixedDepositAccountDetails) {
if (fixedDepositAccountDetails[key] === '') {
delete fixedDepositAccountDetails[key];
}
const value = this.fixedDepositAccountDetailsForm.value;
if (this.selectedAccountType) {
value.accountType = this.selectedAccountType;
}
return fixedDepositAccountDetails;
if (this.selectedCustomerSegment) {
value.customerSegment = this.selectedCustomerSegment;
}
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,
signingAuthority: 'Mandatory',
role: 'Primary Account Holder'
},
...this.jointHolders.map((h) => ({
clientId: h.clientId,
displayName: h.clientName,
cifNumber: 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.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship;
value.minorDateOfBirth = 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;
}
}
@@ -6,161 +6,419 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-column gap-2px">
<div class="flex-fill" *ngIf="fixedDepositsAccountProductTemplate?.accountChart.name">
<span class="flex-40">{{ 'labels.inputs.name' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate.accountChart.name }}</span>
</div>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Valid from Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }}</span>
</div>
<div class="flex-fill" *ngIf="fixedDepositsAccountProductTemplate?.accountChart.endDate">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }}</span>
</div>
<div class="flex-fill" *ngIf="fixedDepositsAccountProductTemplate?.accountChart.description">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.description }}</span>
</div>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}:</span>
<span class="flex-60">{{
fixedDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount ? 'Yes' : 'No'
}}</span>
</div>
<table class="mat-elevation-z1" mat-table [dataSource]="interestRateChartData" #chartsTable multiTemplateDataRows>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.fromPeriod }}-{{ chartSlab.toPeriod }}&nbsp;{{ chartSlab.periodType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.amountRangeFrom }}-{{ chartSlab.amountRangeTo }}</td>
</ng-container>
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
<span *ngIf="expandChartSlabIndex !== chartSlabIndex">
<fa-icon icon="eye" class="m-r-10"></fa-icon>
{{ 'labels.buttons.View Incentives' | translate }}
</span>
<span *ngIf="expandChartSlabIndex === chartSlabIndex">
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Hide Incentives' | translate }}
</span>
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
>
<div
class="incentives layout-row-wrap flex-fill"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-fill">
<h4 class="m-b-10 flex-13">
{{ 'labels.heading.Incentives' | translate }}
</h4>
<table
class="flex-fill mat-elevation-z1"
mat-table
[dataSource]="chartSlab.incentives"
*ngIf="chartSlab.incentives.length"
>
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.entityType.value }}
</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.attributeName.value }}
</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.conditionType.value | titlecase }}
</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive" [ngSwitch]="incentive.attributeName">
{{ incentive.attributeValueDesc }}
</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.incentiveType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.amount }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
<div class="fdrc-wrapper">
<!-- ══ Chart Header Card ════════════════════════════════════════════════ -->
@if (fixedDepositsAccountProductTemplate?.accountChart) {
<div class="fdrc-header-card">
<div class="fdrc-header-card__left">
<div class="fdrc-header-card__icon">
<i class="fas fa-chart-line"></i>
</div>
</td>
</ng-container>
<div>
<div class="fdrc-header-card__title">{{ chartName || 'Interest Rate Chart' }}</div>
@if (chartDescription) {
<div class="fdrc-header-card__desc">{{ chartDescription }}</div>
}
</div>
</div>
<div class="fdrc-header-card__meta">
@if (fdType) {
<span
class="fdrc-pill fdrc-pill--type"
[class.fdrc-pill--single]="fdType === 'SINGLE'"
[class.fdrc-pill--double]="fdType === 'DOUBLE'"
[class.fdrc-pill--special]="fdType === 'SPECIAL'"
>
<i class="fas fa-tag"></i>
{{ fdTypeLabel }}
</span>
}
<div class="fdrc-header-card__dates">
<span class="fdrc-meta-item">
<i class="fas fa-calendar-check"></i>
From: <strong>{{ chartFromDate | dateFormat }}</strong>
</span>
@if (chartEndDate) {
<span class="fdrc-meta-item">
<i class="fas fa-calendar-times"></i>
To: <strong>{{ chartEndDate | dateFormat }}</strong>
</span>
}
</div>
@if (isPrimaryGroupingByAmount) {
<span class="fdrc-pill fdrc-pill--amount"> <i class="fas fa-layer-group"></i> Grouped by Amount </span>
}
</div>
</div>
}
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
</div>
<!-- ══ FD Type Rule Banner ══════════════════════════════════════════════ -->
@if (fdType) {
<div
class="fdrc-rule-banner"
[class.fdrc-rule-banner--single]="fdType === 'SINGLE'"
[class.fdrc-rule-banner--double]="fdType === 'DOUBLE'"
[class.fdrc-rule-banner--special]="fdType === 'SPECIAL'"
>
<div class="fdrc-rule-banner__icon">
@if (fdType === 'SINGLE') {
<i class="fas fa-hourglass-start"></i>
}
@if (fdType === 'DOUBLE') {
<i class="fas fa-hourglass-end"></i>
}
@if (fdType === 'SPECIAL') {
<i class="fas fa-star"></i>
}
</div>
<div class="fdrc-rule-banner__content">
@if (fdType === 'SINGLE') {
<strong>FD Single — Short Term</strong>
<span>Tenure must be less than 1 year (1 to 364 days). Enter tenure in Days only.</span>
}
@if (fdType === 'DOUBLE') {
<strong>FD Double — Long Term</strong>
<span>Tenure must be 1 year or more. Enter tenure in Months or Years (minimum 12 months).</span>
}
@if (fdType === 'SPECIAL') {
<strong>FD Special — Monthly Interest Payout</strong>
<span
>Interest is paid out monthly. Enter your principal to estimate monthly income. Any tenure is allowed.</span
>
}
</div>
</div>
}
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<!-- ══ Tenure Calculator ════════════════════════════════════════════════ -->
<div class="fdrc-calc-card">
<div class="fdrc-calc-card__header">
<div class="fdrc-calc-card__header-left">
<div class="fdrc-calc-icon">
<i class="fas fa-calculator"></i>
</div>
<div>
<span class="fdrc-calc-card__title">Rate Calculator</span>
<span class="fdrc-calc-card__sub">Enter your deposit tenure to find the applicable interest rate</span>
</div>
</div>
</div>
<div class="fdrc-calc-card__body">
<div class="fdrc-calc-inputs">
<!-- Tenure Value -->
<div class="fdrc-calc-field">
<label class="fdrc-calc-label">Deposit Tenure</label>
<div class="fdrc-tenure-row">
<input
type="number"
class="fdrc-tenure-input"
[(ngModel)]="tenureValue"
[min]="1"
placeholder="Enter value"
(keyup.enter)="calculateRate()"
/>
<!-- Period Unit Selector -->
@if (allowedPeriodUnits.length === 1) {
<span class="fdrc-period-badge">{{ allowedPeriodUnits[0] }}</span>
} @else {
<div class="fdrc-period-tabs">
@for (unit of allowedPeriodUnits; track unit) {
<button
type="button"
class="fdrc-period-tab"
[class.fdrc-period-tab--active]="tenurePeriodUnit === unit"
(click)="tenurePeriodUnit = unit; onPeriodUnitChange()"
>
{{ unit }}
</button>
}
</div>
}
</div>
<!-- FD type constraint hint -->
@if (fdType === 'SINGLE') {
<span class="fdrc-calc-hint"><i class="fas fa-info-circle"></i> Max 364 days allowed</span>
}
@if (fdType === 'DOUBLE') {
<span class="fdrc-calc-hint"><i class="fas fa-info-circle"></i> Minimum 12 months required</span>
}
</div>
<!-- Principal Amount (SPECIAL only) -->
@if (isSpecial) {
<div class="fdrc-calc-field">
<label class="fdrc-calc-label"
>Principal Amount <span class="fdrc-calc-optional">(optional — for monthly payout estimate)</span></label
>
<div class="fdrc-amount-row">
<span class="fdrc-amount-prefix">₹</span>
<input
type="number"
class="fdrc-tenure-input fdrc-tenure-input--amount"
[(ngModel)]="principalAmount"
[min]="1"
placeholder="e.g. 100000"
/>
</div>
</div>
}
<div class="fdrc-calc-actions">
<button type="button" class="fdrc-calc-btn" (click)="calculateRate()">
<i class="fas fa-search"></i> Find Rate
</button>
@if (tenureValue || matchResult) {
<button type="button" class="fdrc-calc-reset" (click)="resetCalculator()">
<i class="fas fa-times"></i> Reset
</button>
}
</div>
</div>
<!-- Validation Warning -->
@if (validationWarning) {
<div class="fdrc-alert fdrc-alert--warn">
<i class="fas fa-exclamation-triangle"></i>
<span>{{ validationWarning }}</span>
</div>
}
<!-- Match Result -->
@if (matchResult) {
<div class="fdrc-result-card">
<div class="fdrc-result-card__check">
<i class="fas fa-check-circle"></i>
</div>
<div class="fdrc-result-card__body">
<!-- Selected Tenure Banner -->
<div class="fdrc-tenure-banner">
<i class="fas fa-calendar-check"></i>
<div class="fdrc-tenure-banner__content">
<span class="fdrc-tenure-banner__label">Your Selected FD Tenure</span>
<span class="fdrc-tenure-banner__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
</div>
<span class="fdrc-tenure-banner__saved"> <i class="fas fa-save"></i> Will be saved </span>
</div>
<div class="fdrc-result-rate">
<span class="fdrc-result-rate__value">{{ matchResult.appliedRate | number: '1.2-2' }}%</span>
<span class="fdrc-result-rate__label">Annual Interest Rate</span>
</div>
<div class="fdrc-result-meta">
<div class="fdrc-result-meta__item">
<i class="fas fa-layer-group"></i>
<span
>Applicable Slab:
<strong>
{{ matchResult.slab.fromPeriod }}–{{ matchResult.slab.toPeriod ?? '∞' }}
{{ matchResult.slab.periodType?.value }}
</strong>
</span>
</div>
@if (matchResult.slab.description) {
<div class="fdrc-result-meta__item">
<i class="fas fa-tag"></i>
<span>{{ matchResult.slab.description }}</span>
</div>
}
@if (matchResult.slab.amountRangeFrom || matchResult.slab.amountRangeTo) {
<div class="fdrc-result-meta__item">
<i class="fas fa-coins"></i>
<span
>Amount Range:
<strong
>{{ matchResult.slab.amountRangeFrom | number }}–{{
matchResult.slab.amountRangeTo | number
}}</strong
>
</span>
</div>
}
</div>
<!-- Monthly Payout (SPECIAL) -->
@if (isSpecial && matchResult.estimatedMonthlyPayout != null) {
<div class="fdrc-payout-card">
<i class="fas fa-wallet"></i>
<div class="fdrc-payout-card__content">
<span class="fdrc-payout-card__label">Estimated Monthly Payout</span>
<span class="fdrc-payout-card__amount"
>₹ {{ matchResult.estimatedMonthlyPayout | number: '1.2-2' }}</span
>
<span class="fdrc-payout-card__formula"
>= ₹{{ principalAmount | number }} × {{ matchResult.appliedRate }}% ÷ 12</span
>
</div>
</div>
}
@if (isSpecial && !matchResult.estimatedMonthlyPayout) {
<div class="fdrc-payout-hint">
<i class="fas fa-lightbulb"></i>
Enter a principal amount above to see estimated monthly interest payout.
</div>
}
</div>
</div>
}
</div>
</div>
<!-- ══ Rate Chart Table ══════════════════════════════════════════════════ -->
<div class="fdrc-table-card">
<div class="fdrc-table-card__header">
<div class="fdrc-table-card__title-row">
<i class="fas fa-table"></i>
<span class="fdrc-table-card__title">Interest Rate Slabs</span>
@if (interestRateChartData.length) {
<span class="fdrc-slab-count"
>{{ interestRateChartData.length }} slab{{ interestRateChartData.length !== 1 ? 's' : '' }}</span
>
}
</div>
@if (matchResult) {
<span class="fdrc-highlight-note"> <i class="fas fa-highlighter"></i> Matched slab is highlighted </span>
}
</div>
<div class="fdrc-table-wrap">
<table mat-table [dataSource]="interestRateChartData" #chartsTable multiTemplateDataRows class="fdrc-table">
<!-- Period Column -->
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Period</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td fdrc-td--period">
<span class="fdrc-period-range">
{{ slab.fromPeriod }}
@if (slab.toPeriod != null) {
– {{ slab.toPeriod }}
} @else {
+
}
</span>
<span class="fdrc-period-type">{{ slab.periodType?.value }}</span>
</td>
</ng-container>
<!-- Amount Range Column -->
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Amount Range</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
@if (slab.amountRangeFrom || slab.amountRangeTo) {
<span>{{ slab.amountRangeFrom | number }} – {{ slab.amountRangeTo | number }}</span>
} @else {
<span class="fdrc-td--na">—</span>
}
</td>
</ng-container>
<!-- Interest Column -->
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Interest Rate</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
<span class="fdrc-rate-badge">{{ slab.annualInterestRate | number: '1.2-2' }}%</span>
</td>
</ng-container>
<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Description</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
{{ slab.description || '—' }}
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Incentives</th>
<td mat-cell *matCellDef="let slab; let i = dataIndex" class="fdrc-td">
@if (slab.incentives?.length) {
<button
type="button"
class="fdrc-incentive-btn"
(click)="expandChartSlabIndex = expandChartSlabIndex === i ? null : i"
>
<i
class="fas"
[class.fa-eye]="expandChartSlabIndex !== i"
[class.fa-eye-slash]="expandChartSlabIndex === i"
></i>
{{ expandChartSlabIndex === i ? 'Hide' : 'View' }} ({{ slab.incentives.length }})
</button>
} @else {
<span class="fdrc-td--na">None</span>
}
</td>
</ng-container>
<!-- Incentives Expanded Row -->
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let slab; let i = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
class="fdrc-incentives-cell"
>
<div
class="fdrc-incentives-wrap"
[@expandChartSlab]="i === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
@if (slab.incentives?.length) {
<div class="fdrc-incentives-inner">
<div class="fdrc-incentives-title"><i class="fas fa-gift"></i> Incentives for this slab</div>
<table mat-table [dataSource]="slab.incentives" class="fdrc-incentives-table">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>Entity Type</th>
<td mat-cell *matCellDef="let inc">{{ inc.entityType?.value }}</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>Attribute</th>
<td mat-cell *matCellDef="let inc">{{ inc.attributeName?.value }}</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>Condition</th>
<td mat-cell *matCellDef="let inc">{{ inc.conditionType?.value | titlecase }}</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>Value</th>
<td mat-cell *matCellDef="let inc">{{ inc.attributeValueDesc }}</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let inc">{{ inc.incentiveType?.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>Amount</th>
<td mat-cell *matCellDef="let inc">{{ inc.amount }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</div>
}
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns" class="fdrc-header-row"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsDisplayedColumns; let i = dataIndex"
class="fdrc-data-row"
[class.fdrc-data-row--matched]="isMatchedSlab(i)"
></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="fdrc-incentives-row"
></tr>
</table>
@if (!interestRateChartData.length) {
<div class="fdrc-empty">
<i class="fas fa-chart-bar"></i>
<span>No interest rate slabs defined for this product.</span>
</div>
}
</div>
</div>
</div>
@@ -6,26 +6,866 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.margin-t {
margin-top: 2em;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-100: #f1f5f9;
$gray-200: #e8eaed;
$gray-600: #64748b;
$gray-700: #334155;
$gray-900: #0f172a;
$blue: #2563eb;
$blue-light: #dbeafe;
$green: #16a34a;
$green-light: #dcfce7;
$amber: #d97706;
$amber-light: #fef3c7;
$purple: #7c3aed;
$purple-light: #ede9fe;
$red: #dc2626;
$red-light: #fee2e2;
.fdrc-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
}
table {
width: 100%;
// ── Chart Header Card ──────────────────────────────────────────────────────
tr.incentives-row {
height: 0;
.fdrc-header-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
background: #fff;
border: 1px solid $border;
border-radius: 12px;
padding: 16px 20px;
flex-wrap: wrap;
> td {
padding: 0;
&__left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
background: $blue-light;
color: $blue;
font-size: 18px;
flex-shrink: 0;
}
&__title {
font-size: 15px;
font-weight: 700;
color: $gray-900;
}
&__desc {
font-size: 12px;
color: $gray-600;
margin-top: 2px;
}
&__meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
&__dates {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}
}
.fdrc-meta-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: $gray-700;
i {
color: $gray-600;
}
strong {
color: $gray-900;
}
}
// ── Pills ──────────────────────────────────────────────────────────────────
.fdrc-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
&--type {
background: #f1f5f9;
color: $gray-700;
}
&--single {
background: $amber-light;
color: $amber;
}
&--double {
background: $purple-light;
color: $purple;
}
&--special {
background: $green-light;
color: $green;
}
&--amount {
background: $blue-light;
color: $blue;
}
}
// ── FD Type Rule Banner ────────────────────────────────────────────────────
.fdrc-rule-banner {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 18px;
border-radius: 10px;
border-left: 4px solid;
&--single {
background: $amber-light;
border-color: $amber;
color: #b45309;
}
&--double {
background: $purple-light;
border-color: $purple;
color: #6d28d9;
}
&--special {
background: $green-light;
border-color: $green;
color: #15803d;
}
&__icon {
font-size: 18px;
margin-top: 2px;
flex-shrink: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-size: 13px;
font-weight: 700;
}
.incentives {
overflow: hidden;
}
.mat-card {
border-radius: 0;
span {
font-size: 12px;
}
}
}
// ── Calculator Card ────────────────────────────────────────────────────────
.fdrc-calc-card {
background: #fff;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
}
&__header-left {
display: flex;
align-items: center;
gap: 12px;
}
&__title {
font-size: 14px;
font-weight: 700;
color: $gray-900;
display: block;
}
&__sub {
font-size: 12px;
color: $gray-600;
display: block;
margin-top: 1px;
}
&__body {
padding: 20px;
}
}
.fdrc-calc-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: $blue-light;
color: $blue;
font-size: 16px;
flex-shrink: 0;
}
.fdrc-calc-inputs {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.fdrc-calc-field {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1 1 220px;
min-width: 180px;
}
.fdrc-calc-label {
font-size: 12px;
font-weight: 600;
color: $gray-700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.fdrc-calc-optional {
font-weight: 400;
text-transform: none;
letter-spacing: 0;
color: $gray-600;
}
.fdrc-tenure-row {
display: flex;
align-items: center;
gap: 8px;
}
.fdrc-tenure-input {
height: 42px;
border: 1.5px solid $border;
border-radius: 8px;
padding: 0 12px;
font-size: 15px;
font-weight: 600;
color: $gray-900;
background: #fff;
width: 130px;
transition: border-color 0.15s;
&:focus {
outline: none;
border-color: $blue;
}
&--amount {
width: 180px;
}
}
.fdrc-period-badge {
display: inline-flex;
align-items: center;
padding: 0 14px;
height: 42px;
background: $gray-100;
border: 1.5px solid $border;
border-radius: 8px;
font-size: 13px;
font-weight: 700;
color: $gray-700;
}
.fdrc-period-tabs {
display: flex;
border: 1.5px solid $border;
border-radius: 8px;
overflow: hidden;
}
.fdrc-period-tab {
height: 42px;
padding: 0 14px;
font-size: 13px;
font-weight: 600;
border: none;
background: #fff;
color: $gray-600;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
& + & {
border-left: 1px solid $border;
}
&--active {
background: $blue;
color: #fff;
}
}
.fdrc-amount-row {
display: flex;
align-items: center;
}
.fdrc-amount-prefix {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 42px;
background: $gray-100;
border: 1.5px solid $border;
border-right: none;
border-radius: 8px 0 0 8px;
font-size: 15px;
font-weight: 700;
color: $gray-700;
}
.fdrc-amount-row .fdrc-tenure-input {
border-radius: 0 8px 8px 0;
}
.fdrc-calc-hint {
font-size: 11px;
color: $amber;
display: flex;
align-items: center;
gap: 4px;
}
.fdrc-calc-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
margin-top: 22px;
}
.fdrc-calc-btn {
display: inline-flex;
align-items: center;
gap: 7px;
height: 42px;
padding: 0 22px;
background: $blue;
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
&:hover {
background: #1d4ed8;
}
}
.fdrc-calc-reset {
display: inline-flex;
align-items: center;
gap: 6px;
height: 42px;
padding: 0 16px;
background: transparent;
color: $gray-600;
border: 1.5px solid $border;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
&:hover {
border-color: $red;
color: $red;
}
}
// ── Alert ─────────────────────────────────────────────────────────────────
.fdrc-alert {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
border-radius: 8px;
margin-top: 16px;
font-size: 13px;
i {
margin-top: 1px;
flex-shrink: 0;
}
&--warn {
background: $amber-light;
color: #b45309;
border: 1px solid rgba($amber, 0.4);
}
}
// ── Tenure Banner (inside result card) ───────────────────────────────────
.fdrc-tenure-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: #fff;
border: 1.5px solid rgba($green, 0.4);
border-radius: 8px;
margin-bottom: 14px;
> i {
font-size: 18px;
color: $green;
flex-shrink: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 1px;
flex: 1;
}
&__label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $gray-600;
}
&__value {
font-size: 20px;
font-weight: 800;
color: $gray-900;
line-height: 1.2;
}
&__saved {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: $green-light;
color: $green;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
flex-shrink: 0;
}
}
// ── Result Card ───────────────────────────────────────────────────────────────
.fdrc-result-card {
display: flex;
align-items: flex-start;
gap: 16px;
margin-top: 16px;
padding: 18px 20px;
background: $green-light;
border: 1.5px solid rgba($green, 0.35);
border-radius: 12px;
&__check {
font-size: 26px;
color: $green;
flex-shrink: 0;
margin-top: 2px;
}
&__body {
flex: 1;
min-width: 0;
}
}
.fdrc-result-rate {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 10px;
&__value {
font-size: 28px;
font-weight: 800;
color: $green;
line-height: 1;
}
&__label {
font-size: 13px;
color: #166534;
font-weight: 600;
}
}
.fdrc-result-meta {
display: flex;
flex-direction: column;
gap: 5px;
&__item {
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: $gray-700;
i {
color: $green;
font-size: 11px;
}
strong {
color: $gray-900;
}
}
}
// ── Monthly Payout (SPECIAL) ───────────────────────────────────────────────
.fdrc-payout-card {
display: flex;
align-items: center;
gap: 14px;
margin-top: 14px;
padding: 14px 16px;
background: #fff;
border: 1.5px solid rgba($green, 0.3);
border-radius: 10px;
i {
font-size: 22px;
color: $green;
flex-shrink: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 2px;
}
&__label {
font-size: 11px;
font-weight: 600;
color: $gray-600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
&__amount {
font-size: 22px;
font-weight: 800;
color: $green;
line-height: 1.2;
}
&__formula {
font-size: 11px;
color: $gray-600;
font-style: italic;
}
}
.fdrc-payout-hint {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 12px;
color: $gray-600;
font-style: italic;
i {
color: $amber;
}
}
// ── Rate Chart Table Card ─────────────────────────────────────────────────
.fdrc-table-card {
background: #fff;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
flex-wrap: wrap;
gap: 8px;
}
&__title-row {
display: flex;
align-items: center;
gap: 10px;
i {
color: $blue;
}
}
&__title {
font-size: 14px;
font-weight: 700;
color: $gray-900;
}
}
.fdrc-slab-count {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: $blue-light;
color: $blue;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
.fdrc-highlight-note {
font-size: 12px;
color: $green;
font-weight: 600;
display: flex;
align-items: center;
gap: 5px;
}
.fdrc-table-wrap {
overflow-x: auto;
}
.fdrc-table {
width: 100%;
min-width: 600px;
}
.fdrc-th {
font-size: 11px !important;
font-weight: 700 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $gray-700 !important;
background: $gray-50;
padding: 10px 16px !important;
}
.fdrc-td {
padding: 12px 16px !important;
font-size: 13px;
color: $gray-700;
vertical-align: middle;
&--period {
display: flex;
flex-direction: column;
gap: 2px;
}
&--na {
color: #94a3b8;
}
}
.fdrc-period-range {
font-weight: 700;
color: $gray-900;
font-size: 13px;
}
.fdrc-period-type {
font-size: 11px;
color: $gray-600;
}
.fdrc-rate-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
background: $blue-light;
color: $blue;
border-radius: 999px;
font-size: 13px;
font-weight: 700;
}
.fdrc-incentive-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
height: 32px;
background: transparent;
border: 1.5px solid $border;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: $gray-700;
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
&:hover {
border-color: $blue;
color: $blue;
}
}
.fdrc-header-row {
background: $gray-50;
}
.fdrc-data-row {
transition: background 0.15s;
&:hover td {
background: $gray-50;
}
&--matched td {
background: $green-light !important;
border-left: 3px solid $green;
}
}
.fdrc-incentives-row {
height: 0;
> td {
padding: 0 !important;
}
}
.fdrc-incentives-cell {
padding: 0 !important;
overflow: hidden;
}
.fdrc-incentives-wrap {
overflow: hidden;
}
.fdrc-incentives-inner {
padding: 12px 20px 16px;
background: $gray-50;
border-top: 1px dashed $border;
}
.fdrc-incentives-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 700;
color: $gray-700;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
i {
color: $purple;
}
}
.fdrc-incentives-table {
width: 100%;
background: #fff;
border: 1px solid $border;
border-radius: 8px;
overflow: hidden;
}
.fdrc-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 20px;
color: #94a3b8;
i {
font-size: 32px;
}
span {
font-size: 14px;
}
}
// ── Stepper Actions ───────────────────────────────────────────────────────
.fdrc-stepper-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 8px;
flex-wrap: wrap;
}
@@ -6,8 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, Input, OnChanges, ViewChild } from '@angular/core';
import { Component, Input, OnChanges, Output, EventEmitter, ViewChild } from '@angular/core';
import {
MatTableDataSource,
MatTable,
@@ -22,15 +21,20 @@ import {
MatRow
} from '@angular/material/table';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { NgIf, NgSwitch, TitleCasePipe } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { NgIf, NgSwitch, TitleCasePipe, DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Fixed Deposits Account Interest Rate Chart Step
*/
export type FdPeriodUnit = 'Days' | 'Months' | 'Years';
export interface SlabMatchResult {
slab: any;
slabIndex: number;
appliedRate: number;
estimatedMonthlyPayout?: number;
}
@Component({
selector: 'mifosx-fixed-deposit-account-interest-rate-chart-step',
templateUrl: './fixed-deposit-account-interest-rate-chart-step.component.html',
@@ -44,41 +48,38 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
FaIconComponent,
NgSwitch,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
MatStepperNext,
TitleCasePipe,
DateFormatPipe
DateFormatPipe,
DecimalPipe
]
})
export class FixedDepositAccountInterestRateChartStepComponent implements OnChanges {
/** Fixed deposits account template */
@Input() fixedDepositsAccountTemplate: any;
/** Fixed deposits account and product template */
@Input() fixedDepositsAccountProductTemplate: any;
/** Interest Rate Chart Data */
interestRateChartData: any = [];
/** Columns to be displayed in interest rate chart table. */
chartSlabsDisplayedColumns: any[] = [
@Output() tenureSelected = new EventEmitter<{ depositPeriod: number; depositPeriodFrequencyId: number }>();
interestRateChartData: any[] = [];
chartSlabsDisplayedColumns: string[] = [
'period',
'amountRange',
'interest',
'description',
'actions'
];
/** Columns to be displayed in incentives sub-table. */
incentivesDisplayedColumns: string[] = [
'entityType',
'attributeName',
@@ -87,20 +88,196 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
'incentiveType',
'amount'
];
/** Additional Column to disblac incentives table */
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
/** Expand Chart Slab Index used in the view */
expandChartSlabIndex: number;
/** Interest Rate charts table reference */
expandChartSlabIndex: number | null = null;
@ViewChild('chartsTable', { static: true }) chartsTableRef: MatTable<Element>;
constructor() {}
// ── Tenure Calculator ───────────────────────────────────────────────────
tenureValue: number | null = null;
tenurePeriodUnit: FdPeriodUnit = 'Days';
principalAmount: number | null = null;
matchResult: SlabMatchResult | null = null;
validationWarning: string | null = null;
// ── FD Type labels ──────────────────────────────────────────────────────
readonly fdTypeLabels: Record<string, string> = {
SINGLE: 'FD Single (Days < 1 Year)',
DOUBLE: 'FD Double (≥ 1 Year)',
SPECIAL: 'FD Special (Monthly Interest Payout)'
};
get fdType(): string {
return this.fixedDepositsAccountProductTemplate?.fdType || '';
}
get fdTypeLabel(): string {
return this.fdTypeLabels[this.fdType] || this.fdType;
}
/** Period unit options driven by fdType */
get allowedPeriodUnits(): FdPeriodUnit[] {
if (this.fdType === 'SINGLE') return ['Days'];
if (this.fdType === 'DOUBLE') return [
'Months',
'Years'
];
return [
'Days',
'Months',
'Years'
];
}
get isSpecial(): boolean {
return this.fdType === 'SPECIAL';
}
ngOnChanges() {
if (this.fixedDepositsAccountProductTemplate) {
this.interestRateChartData = this.fixedDepositsAccountProductTemplate.accountChart.chartSlabs;
this.chartsTableRef.renderRows();
this.interestRateChartData = this.fixedDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
if (this.chartsTableRef) {
this.chartsTableRef.renderRows();
}
// Reset calculator when product changes
this.resetCalculator();
// Set default period unit based on FD type
this.tenurePeriodUnit = this.allowedPeriodUnits[0];
}
}
// Convert entered tenure to Days for slab matching
private toDays(value: number, unit: FdPeriodUnit): number {
if (unit === 'Days') return value;
if (unit === 'Months') return value * 30;
return value * 365;
}
// Convert slab period (which is stored in its own periodType) to days
private slabPeriodToDays(period: number, periodTypeValue: string): number {
const p = (periodTypeValue || '').toLowerCase();
if (p.includes('month')) return period * 30;
if (p.includes('year')) return period * 365;
return period; // days
}
calculateRate() {
this.validationWarning = null;
this.matchResult = null;
if (!this.tenureValue || this.tenureValue <= 0) {
this.validationWarning = 'Please enter a valid tenure value greater than 0.';
return;
}
// FD Type specific validation
const enteredDays = this.toDays(this.tenureValue, this.tenurePeriodUnit);
if (this.fdType === 'SINGLE' && enteredDays >= 365) {
this.validationWarning =
'FD Single allows tenure less than 1 year (max 364 days). Please enter a shorter tenure.';
return;
}
if (this.fdType === 'DOUBLE' && enteredDays < 365) {
this.validationWarning =
'FD Double requires tenure of at least 1 year (minimum 12 months). Please enter a longer tenure.';
return;
}
const slabs: any[] = this.interestRateChartData || [];
if (!slabs.length) {
this.validationWarning = 'No interest rate slabs are defined for this product.';
return;
}
let matchedSlab: any = null;
let matchedIndex = -1;
for (let i = 0; i < slabs.length; i++) {
const slab = slabs[i];
const periodTypeValue = slab.periodType?.value || 'Days';
const fromDays = this.slabPeriodToDays(slab.fromPeriod ?? 0, periodTypeValue);
const toDays = slab.toPeriod != null ? this.slabPeriodToDays(slab.toPeriod, periodTypeValue) : Infinity;
if (enteredDays >= fromDays && enteredDays <= toDays) {
matchedSlab = slab;
matchedIndex = i;
break;
}
}
if (!matchedSlab) {
this.validationWarning =
'No applicable interest rate slab found for the entered tenure. Please check the rate chart below.';
return;
}
const rate = matchedSlab.annualInterestRate ?? 0;
const result: SlabMatchResult = {
slab: matchedSlab,
slabIndex: matchedIndex,
appliedRate: rate
};
if (this.isSpecial && this.principalAmount && this.principalAmount > 0) {
result.estimatedMonthlyPayout = (this.principalAmount * rate) / 1200;
}
this.matchResult = result;
// Emit selected tenure to parent for backend submission
const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit);
if (frequencyId !== null) {
this.tenureSelected.emit({ depositPeriod: this.tenureValue, depositPeriodFrequencyId: frequencyId });
}
}
private resolveFrequencyId(unit: FdPeriodUnit): number | null {
const options: any[] = this.fixedDepositsAccountProductTemplate?.periodFrequencyTypeOptions ?? [];
const unitLower = unit.toLowerCase();
const match = options.find((o: any) => (o.value || '').toLowerCase().includes(unitLower));
if (match) return match.id;
// Fineract standard fallback: Days=0, Months=2, Years=3
const fallback: Record<FdPeriodUnit, number> = { Days: 0, Months: 2, Years: 3 };
return fallback[unit] ?? null;
}
resetCalculator() {
this.tenureValue = null;
this.principalAmount = null;
this.matchResult = null;
this.validationWarning = null;
}
onPeriodUnitChange() {
this.matchResult = null;
this.validationWarning = null;
}
isMatchedSlab(index: number): boolean {
return this.matchResult?.slabIndex === index;
}
get chartName(): string {
return this.fixedDepositsAccountProductTemplate?.accountChart?.name || '';
}
get chartFromDate(): any {
return this.fixedDepositsAccountProductTemplate?.accountChart?.fromDate;
}
get chartEndDate(): any {
return this.fixedDepositsAccountProductTemplate?.accountChart?.endDate;
}
get chartDescription(): string {
return this.fixedDepositsAccountProductTemplate?.accountChart?.description || '';
}
get isPrimaryGroupingByAmount(): boolean {
return !!this.fixedDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount;
}
}
@@ -6,416 +6,587 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Details' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Product' | translate }}</span>
<span class="flex-60">{{
fixedDepositAccountData.productId | find: fixedDepositsAccountTemplate.productOptions : 'id' : 'name'
}}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Submitted On' | translate }}</span>
<span class="flex-60">{{ fixedDepositAccountData.submittedOnDate | dateFormat }}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositAccountData.externalId">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span>
<span class="flex-60">{{ fixedDepositAccountData.externalId }}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Field Officer' | translate }}</span>
<span class="flex-60">{{
(fixedDepositAccountData.fieldOfficerId
| find: fixedDepositsAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName') || 'Unassigned'
| translateKey: 'inputs'
}}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Currency' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate.currency.code }}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Currency Multiple' | translate }}</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate.currency.inMultiplesOf }}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate.currency.decimalPlaces }}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Fixed Deposit Amount' | translate }}:</span>
<span class="flex-60">{{
fixedDepositAccountData.depositAmount
| currency: fixedDepositsAccountProductTemplate.currency.code : 'symbol-narrow' : '1.2-2'
}}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Deposit Period' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositAccountData.depositPeriod }}
{{
fixedDepositAccountData.depositPeriodFrequencyId
| find: fixedDepositsAccountProductTemplate.periodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span
>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{
fixedDepositAccountData.interestCompoundingPeriodType
| find: fixedDepositsAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{
fixedDepositAccountData.interestPostingPeriodType
| find: fixedDepositsAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{
fixedDepositAccountData.interestCalculationType
| find: fixedDepositsAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{
fixedDepositAccountData.interestCalculationDaysInYearType
| find: fixedDepositsAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row flex-100" *ngIf="fixedDepositAccountData.lockinPeriodFrequency">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositAccountData.lockinPeriodFrequency }}
{{
fixedDepositAccountData.lockinPeriodFrequencyType
| find: fixedDepositsAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span
>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate.minDepositTerm">
<span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositsAccountProductTemplate.minDepositTerm }}
{{ fixedDepositsAccountProductTemplate.minDepositTermType?.value | translateKey: 'catalogs' }}</span
>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate.inMultiplesOfDepositTerm">
<span class="flex-40">{{ 'labels.inputs.In Multiples Of' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositsAccountProductTemplate.inMultiplesOfDepositTerm }}
{{ fixedDepositsAccountProductTemplate.inMultiplesOfDepositTermType?.value | translateKey: 'catalogs' }}</span
>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate.maxDepositTerm">
<span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}</span>
<span class="flex-60"
>{{ fixedDepositsAccountProductTemplate.maxDepositTerm }}
{{ fixedDepositsAccountProductTemplate.maxDepositTermType?.value | translateKey: 'catalogs' }}</span
>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Transfer Interest to Savings Account' | translate }}?</span>
<span class="flex-60">{{ fixedDepositAccountData.transferInterestToSavings | yesNo }}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositAccountData.transferInterestToSavings">
<span class="flex-40">{{ 'labels.inputs.Linked Savings Account' | translate }}</span>
<span class="flex-60">{{
fixedDepositAccountData.linkAccountId
| find: fixedDepositsAccountProductTemplate.savingsAccounts : 'id' : 'accountNo'
}}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositAccountData.maturityInstructionId">
<span class="flex-40">{{ 'labels.inputs.Maturity Instructions' | translate }}</span>
<span class="flex-60">{{
fixedDepositAccountData.maturityInstructionId
| find: fixedDepositsAccountProductTemplate.maturityInstructionOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate.preClosurePenalApplicable | yesNo }}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate.preClosurePenalApplicable">
<span class="flex-40">{{ 'labels.inputs.Penal Interest' | translate }} (%):</span>
<span class="flex-60">{{
fixedDepositsAccountProductTemplate.preClosurePenalInterest +
'% (' +
fixedDepositsAccountProductTemplate.preClosurePenalInterestOnType?.id +
')'
}}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate.withHoldTax">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
<span class="flex-60">{{ fixedDepositAccountData.withHoldTax | yesNo }}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Chart' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate?.accountChart.name">
<span class="flex-40">{{ 'labels.inputs.name' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.name }}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Valid from Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate?.accountChart.endDate">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }}</span>
</div>
<div class="layout-row flex-100" *ngIf="fixedDepositsAccountProductTemplate?.accountChart.description">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ fixedDepositsAccountProductTemplate?.accountChart.description }}</span>
</div>
<div class="layout-row flex-100">
<span class="flex-40">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}:</span>
<span class="flex-60">{{
fixedDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo
}}</span>
</div>
<table class="mat-elevation-z1 irc-table" mat-table [dataSource]="interestRateChartData" multiTemplateDataRows>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }}&nbsp;{{
chartSlab.periodType.value | translateKey: 'catalogs'
}}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate | formatNumber }} %</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
<span *ngIf="expandChartSlabIndex !== chartSlabIndex">
<fa-icon icon="eye" class="m-r-10"></fa-icon>
{{ 'labels.buttons.View Incentives' | translate }}
</span>
<span *ngIf="expandChartSlabIndex === chartSlabIndex">
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Hide Incentives' | translate }}
</span>
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
>
<div
class="layout-row-wrap flex-100 incentives"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<h4 class="m-b-10 flex-13">
{{ 'labels.heading.Incentives' | translate }}
</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="chartSlab.incentives"
*ngIf="chartSlab.incentives.length"
>
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.entityType.value }}
</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.attributeName.value }}
</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.conditionType.value | titlecase }}
</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive" [ngSwitch]="incentive.attributeName">
{{ incentive.attributeValueDesc }}
</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.incentiveType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.amount | formatNumber }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
<div class="preview-wrapper">
<!-- ══ 1. Account Details ════════════════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-info-circle preview-section__icon"></i>
<span class="preview-section__title">Account Details</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Product</span>
<span class="preview-value">{{
fixedDepositAccountData?.productId | find: fixedDepositsAccountTemplate?.productOptions : 'id' : 'name'
}}</span>
</div>
</td>
</ng-container>
<div class="preview-field">
<span class="preview-label">Submitted On</span>
<span class="preview-value">{{ fixedDepositAccountData?.submittedOnDate | dateFormat }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Field Officer</span>
<span class="preview-value">{{
(fixedDepositAccountData?.fieldOfficerId
| find: fixedDepositsAccountProductTemplate?.fieldOfficerOptions : 'id' : 'displayName') || 'Unassigned'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">External ID</span>
<span class="preview-value" [class.preview-muted]="!fixedDepositAccountData?.externalId">
{{ fixedDepositAccountData?.externalId || 'Unassigned' }}
</span>
</div>
</div>
</div>
</div>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
<!-- ══ 1a. Product Classification ════════════════════════════════ -->
@if (fixedDepositsAccountProductTemplate) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-tags preview-section__icon"></i>
<span class="preview-section__title">Product Classification</span>
<span class="preview-section__badge preview-section__badge--active" style="margin-left: auto"
>From Product</span
>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (fixedDepositAccountData?.accountType) {
<div class="preview-field">
<span class="preview-label">Account Type</span>
<span class="preview-value">{{ fixedDepositAccountData.accountType }}</span>
</div>
}
@if (fixedDepositAccountData?.customerSegment) {
<div class="preview-field">
<span class="preview-label">Customer Segment</span>
<span class="preview-value">{{ fixedDepositAccountData.customerSegment }}</span>
</div>
}
@if (fixedDepositsAccountProductTemplate?.fdType) {
<div class="preview-field">
<span class="preview-label">FD Type</span>
<span class="preview-value">{{ getFdTypeLabel(fixedDepositsAccountProductTemplate.fdType) }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Account No</span>
<span class="preview-value preview-mono">
{{
fixedDepositsAccountProductTemplate?.previewAccountNo ||
fixedDepositsAccountTemplate?.accountNo ||
'Auto-generated'
}}
</span>
</div>
@if (fixedDepositsAccountProductTemplate?.effectiveDate) {
<div class="preview-field">
<span class="preview-label">Effective To</span>
<span class="preview-value">{{ fixedDepositsAccountProductTemplate.effectiveDate | dateFormat }}</span>
</div>
}
@if (fixedDepositsAccountProductTemplate?.endDate) {
<div class="preview-field">
<span class="preview-label">End Date</span>
<span class="preview-value">{{ fixedDepositsAccountProductTemplate.endDate | dateFormat }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Share Linking Enabled</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="fixedDepositsAccountProductTemplate?.shareLinkingEnabled"
>
{{ fixedDepositsAccountProductTemplate?.shareLinkingEnabled ? 'Yes' : 'No' }}
</span>
</div>
@if (fixedDepositsAccountProductTemplate?.introducerRequired !== undefined) {
<div class="preview-field">
<span class="preview-label">Introducer Required</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="fixedDepositsAccountProductTemplate?.introducerRequired"
>
{{ fixedDepositsAccountProductTemplate?.introducerRequired ? 'Yes' : 'No' }}
</span>
</div>
}
</div>
</div>
</div>
}
<div *ngIf="fixedDepositAccountData.charges.length" class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Charges' | translate }}</h3>
<!-- ══ 1b. Status of Account (RBI) ══════════════════════════════ -->
@if (fixedDepositAccountData?.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">
<div class="preview-soa-status-row">
<span class="preview-soa-tile">
@switch (fixedDepositAccountData.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 {
{{ fixedDepositAccountData.accountStatus }}
}
}
</span>
</div>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fixedDepositAccountData.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
@if (fixedDepositAccountData.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 (fixedDepositAccountData.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 {
{{ fixedDepositAccountData.modeOfOperation || '—' }}
}
}
</span>
</div>
</div>
@if (fixedDepositAccountData.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 fixedDepositAccountData.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 || holder.clientName || '—' }}
@if (holder.isPrimary) {
<span class="preview-soa-primary-tag">Primary</span>
}
</td>
<td class="preview-soa-td preview-mono">{{ holder.cifNumber || holder.clientCif || '—' }}</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>
}
}
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
@if (fixedDepositAccountData.accountStatus === 'INDIVIDUAL_MALE' && fixedDepositAccountData.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">{{ fixedDepositAccountData.specialCategory }}</span>
</div>
</div>
}
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount }}
</td>
</ng-container>
@if (fixedDepositAccountData.accountStatus === 'INDIVIDUAL_FEMALE' && fixedDepositAccountData.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">{{ fixedDepositAccountData.weakerSection }}</span>
</div>
</div>
}
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (fixedDepositAccountData.accountStatus === 'MINOR') {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Guardian Name</span>
<span class="preview-value">{{ fixedDepositAccountData.guardianName || '—' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Guardian CIF</span>
<span class="preview-value preview-mono">{{ fixedDepositAccountData.guardianCifNumber || '—' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Relationship</span>
<span class="preview-value">{{ fixedDepositAccountData.guardianRelationship || '—' }}</span>
</div>
</div>
}
</div>
</div>
}
<!-- ══ 1c. Linked Account ════════════════════════════════════════ -->
@if (fixedDepositAccountData?.linkedAccountId) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-link preview-section__icon"></i>
<span class="preview-section__title">Linked Deposit Account</span>
<span class="preview-section__badge">{{ fixedDepositAccountData.linkedAccountType }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Account No</span>
<span class="preview-value preview-mono">{{ fixedDepositAccountData.linkedAccountNo }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Account Type</span>
<span class="preview-value">{{ fixedDepositAccountData.linkedAccountType }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Account ID</span>
<span class="preview-value preview-mono">{{ fixedDepositAccountData.linkedAccountId }}</span>
</div>
</div>
</div>
</div>
}
<!-- ══ 2. Currency & Configuration ══════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-coins preview-section__icon"></i>
<span class="preview-section__title">{{ 'labels.heading.Currency' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="preview-value">{{ currencyName }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="preview-value">{{ decimalPlaces }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Currency In Multiples Of' | translate }}</span>
<span class="preview-value">{{ inMultiplesOf }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Auto Debit Facility</span>
<span
*ngIf="charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee'"
class="preview-value preview-badge"
[class.preview-badge--yes]="fixedDepositAccountData?.autoDebitFacility"
>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
<span *ngIf="charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee'">
{{ charge.feeOnMonthDay ? ([2000].concat(charge.feeOnMonthDay) | dateFormat: 'dd MMMM') : 'Unassigned' }}
{{ fixedDepositAccountData?.autoDebitFacility | yesNo }}
</span>
</div>
</div>
</div>
</div>
<!-- ══ 2b. Dormancy Monitoring ═══════════════════════════════════ -->
@if (fixedDepositsAccountProductTemplate?.dormancyAlert) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-moon preview-section__icon"></i>
<span class="preview-section__title">Dormancy Monitoring</span>
<span class="preview-section__badge preview-section__badge--active">Monitoring: Active</span>
</div>
<div class="preview-body">
<div class="preview-dormancy-timeline">
<div class="preview-dorm-step">
<div class="preview-dorm-step__dot preview-dorm-step__dot--inactive"></div>
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Inactive</span>
<span class="preview-dorm-step__days">
After {{ fixedDepositsAccountProductTemplate?.daysToInactiveFromProduct || 30 }} days of no transaction
</span>
</div>
</div>
<div class="preview-dorm-connector"></div>
<div class="preview-dorm-step">
<div class="preview-dorm-step__dot preview-dorm-step__dot--dormant"></div>
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Dormant</span>
<span class="preview-dorm-step__days">
{{ fixedDepositsAccountProductTemplate?.daysToDormancyFromProduct || 60 }} days after inactive
</span>
</div>
</div>
<div class="preview-dorm-connector"></div>
<div class="preview-dorm-step">
<div class="preview-dorm-step__dot preview-dorm-step__dot--escheat"></div>
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Escheat</span>
<span class="preview-dorm-step__days">
{{ fixedDepositsAccountProductTemplate?.daysToEscheatFromProduct || 90 }} days after dormant
</span>
</div>
</div>
</div>
</div>
</div>
}
<!-- ══ 2c. Nominee Details ══════════════════════════════════════ -->
@if (fixedDepositAccountData?.nominee) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-user-shield preview-section__icon"></i>
<span class="preview-section__title">Nominee Details</span>
<span class="preview-section__badge">{{
fixedDepositAccountData.nominee.mode === 'linked' ? 'Linked Client' : 'Manual Entry'
}}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Full Name</span>
<span class="preview-value">{{
fixedDepositAccountData.nominee.fullName || fixedDepositAccountData.nominee.displayName
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">Relationship</span>
<span class="preview-value">{{ fixedDepositAccountData.nominee.relationship }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Nominee Share (%)</span>
<span class="preview-value">{{ fixedDepositAccountData.nominee.nomineeShare }}%</span>
</div>
@if (fixedDepositAccountData.nominee.dateOfBirth) {
<div class="preview-field">
<span class="preview-label">Date of Birth</span>
<span class="preview-value">{{ fixedDepositAccountData.nominee.dateOfBirth | dateFormat }}</span>
</div>
}
@if (fixedDepositAccountData.nominee.nationality) {
<div class="preview-field">
<span class="preview-label">Nationality</span>
<span class="preview-value">{{ fixedDepositAccountData.nominee.nationality }}</span>
</div>
}
@if (fixedDepositAccountData.nominee.mobileNo) {
<div class="preview-field">
<span class="preview-label">Mobile Number</span>
<span class="preview-value preview-mono">{{ fixedDepositAccountData.nominee.mobileNo }}</span>
</div>
}
</div>
</div>
</div>
}
<!-- ══ 5. Interest Rate Chart ═══════════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-chart-line preview-section__icon"></i>
<span class="preview-section__title">{{ 'labels.heading.Interest Rate Chart' | translate }}</span>
@if (selectedTenure) {
<span class="preview-section__badge preview-section__badge--tenure">
<i class="fas fa-calendar-check"></i> Selected Tenure: {{ selectedTenureLabel }}
</span>
}
</div>
<div class="preview-body">
<!-- Selected Tenure + Rate Banner -->
@if (selectedTenure && matchedSlabIndex >= 0) {
<div class="preview-tenure-banner">
<div class="preview-tenure-banner__left">
<i class="fas fa-calendar-check"></i>
<div>
<span class="preview-tenure-banner__label">Your FD Tenure</span>
<span class="preview-tenure-banner__value">{{ selectedTenureLabel }}</span>
</div>
</div>
<div class="preview-tenure-banner__divider"></div>
<div class="preview-tenure-banner__right">
<span class="preview-tenure-banner__rate-label">Applicable Interest Rate</span>
<span class="preview-tenure-banner__rate"
>{{ interestRateChartData[matchedSlabIndex]?.annualInterestRate | formatNumber }}%</span
>
</div>
<div class="preview-tenure-banner__slab">
<i class="fas fa-layer-group"></i>
Slab: {{ interestRateChartData[matchedSlabIndex]?.fromPeriod }}–{{
interestRateChartData[matchedSlabIndex]?.toPeriod ?? '∞'
}}
{{ interestRateChartData[matchedSlabIndex]?.periodType?.value }}
</div>
</div>
}
<div class="preview-grid preview-grid--three" style="margin-bottom: 1rem">
@if (fixedDepositsAccountProductTemplate?.accountChart?.name) {
<div class="preview-field">
<span class="preview-label">Name</span>
<span class="preview-value">{{ fixedDepositsAccountProductTemplate.accountChart.name }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Valid From</span>
<span class="preview-value">{{
fixedDepositsAccountProductTemplate?.accountChart?.fromDate | dateFormat
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">Primary Grouping by Amount</span>
<span
*ngIf="
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
"
class="preview-value preview-badge"
[class.preview-badge--yes]="fixedDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount"
>
{{ 'labels.inputs.N/A' | translate }}
{{ fixedDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount | yesNo }}
</span>
</td>
</ng-container>
</div>
</div>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.feeInterval || 'Not Provided' }}
</td>
</ng-container>
<table
class="mat-elevation-z1 irc-table preview-table"
mat-table
[dataSource]="interestRateChartData"
multiTemplateDataRows
>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }}&nbsp;{{
chartSlab.periodType?.value | translateKey: 'catalogs'
}}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate | formatNumber }} %</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
<fa-icon [icon]="expandChartSlabIndex === chartSlabIndex ? 'eye-slash' : 'eye'" class="m-r-10"></fa-icon>
{{
(expandChartSlabIndex === chartSlabIndex
? 'labels.buttons.Hide Incentives'
: 'labels.buttons.View Incentives'
) | translate
}}
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns?.length"
>
<div
class="layout-row-wrap flex-100 incentives"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<h4 class="m-b-10 flex-13">{{ 'labels.heading.Incentives' | translate }}</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="chartSlab.incentives"
*ngIf="chartSlab.incentives?.length"
>
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.entityType?.value }}</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeName?.value }}</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.conditionType?.value | titlecase }}</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeValueDesc }}</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.incentiveType?.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.amount | formatNumber }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsDisplayedColumns; let i = dataIndex"
[class.preview-slab-row--matched]="i === matchedSlabIndex"
></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
@if (selectedTenure && matchedSlabIndex >= 0) {
<div class="preview-slab-legend">
<span class="preview-slab-legend__dot"></span>
Highlighted row is the slab applicable for your selected tenure of <strong>{{ selectedTenureLabel }}</strong>
</div>
}
</div>
</div>
</div>
@@ -6,35 +6,277 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
// ── Wrapper ──────────────────────────────────────────────────────────────────
.preview-wrapper {
display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 0.5rem 0;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
// ── Section card ─────────────────────────────────────────────────────────────
.preview-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
&__header {
display: flex;
align-items: center;
gap: 8px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0.6rem 1rem;
}
&__icon {
font-size: 13px;
color: #0ea5e9;
width: 16px;
text-align: center;
}
&__title {
flex: 1;
font-size: 0.82rem;
font-weight: 700;
color: #1e293b;
text-transform: uppercase;
letter-spacing: 0.06em;
}
&__badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
text-transform: uppercase;
&--active {
background: #dcfce7 !important;
color: #15803d !important;
border-color: #bbf7d0 !important;
}
}
}
h2,
h3,
h4 {
margin: 0;
// ── Body ─────────────────────────────────────────────────────────────────────
.preview-body {
padding: 1rem 1.25rem;
}
// ── Subsection (inside Settings) ─────────────────────────────────────────────
.preview-subsection {
margin-bottom: 1rem;
&__title {
font-size: 0.82rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #666;
margin-bottom: 0.6rem;
}
}
.preview-divider {
margin: 0.75rem 0 1rem;
}
// ── Grid ─────────────────────────────────────────────────────────────────────
.preview-grid {
display: grid;
gap: 0.85rem 1.5rem;
&--three {
grid-template-columns: repeat(3, 1fr);
@media (width <= 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 480px) {
grid-template-columns: 1fr;
}
}
}
// ── Field ─────────────────────────────────────────────────────────────────────
.preview-field {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.preview-label {
font-size: 0.68rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.preview-value {
font-size: 0.9rem;
color: #1e293b;
font-weight: 500;
}
span {
margin: 0.5em 0;
// ── Yes/No badge ─────────────────────────────────────────────────────────────
.preview-badge {
display: inline-block;
padding: 0.15rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
background: #fef2f2;
color: #dc2626;
width: fit-content;
&--yes {
background: #f0fdf4;
color: #15803d;
}
&--warn {
background: #fef3c7;
color: #92400e;
}
}
mat-divider {
margin: 0 0 0.5em;
// ── Section badge variant for tenure ─────────────────────────────────────────
.preview-section__badge--tenure {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #dcfce7;
color: #16a34a;
margin-left: auto;
}
.margin-t {
margin-top: 1em;
// ── Tenure + Rate banner ──────────────────────────────────────────────────────
.preview-tenure-banner {
display: flex;
align-items: center;
gap: 20px;
padding: 14px 18px;
background: #f0fdf4;
border: 1.5px solid #86efac;
border-radius: 10px;
margin-bottom: 16px;
flex-wrap: wrap;
&__left {
display: flex;
align-items: center;
gap: 10px;
> i {
font-size: 20px;
color: #16a34a;
}
}
&__label {
display: block;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #4b5563;
}
&__value {
display: block;
font-size: 22px;
font-weight: 800;
color: #0f172a;
line-height: 1.2;
}
&__divider {
width: 1px;
height: 40px;
background: #86efac;
}
&__right {
display: flex;
flex-direction: column;
gap: 2px;
}
&__rate-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #4b5563;
}
&__rate {
font-size: 26px;
font-weight: 800;
color: #16a34a;
line-height: 1.2;
}
&__slab {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #374151;
margin-left: auto;
i {
color: #16a34a;
font-size: 11px;
}
}
}
.margin-b {
margin-bottom: 0.25em;
// ── Matched slab row highlight ────────────────────────────────────────────────
.preview-slab-row--matched td {
background: #dcfce7 !important;
border-left: 3px solid #16a34a;
}
// ── Legend below table ────────────────────────────────────────────────────────
.preview-slab-legend {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 12px;
color: #4b5563;
&__dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 2px;
background: #dcfce7;
border: 2px solid #16a34a;
flex-shrink: 0;
}
}
// ── Tables ───────────────────────────────────────────────────────────────────
.preview-table {
width: 100%;
margin-top: 1rem;
}
.irc-table {
@@ -56,3 +298,222 @@ mat-divider {
}
}
}
.margin-t {
margin-top: 1em;
}
// ── Preview Tenure Result Card ─────────────────────────────────────────────────
.preview-tenure-result {
border: 1px solid #e0f2fe;
border-radius: 10px;
overflow: hidden;
&__row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.65rem 1rem;
border-bottom: 1px solid #f0f9ff;
&:last-child {
border-bottom: none;
}
&--highlight {
background: #f0f9ff;
}
}
&__rate {
font-size: 1.5rem;
font-weight: 800;
color: #0369a1;
}
}
// ── Misc ──────────────────────────────────────────────────────────────────────
.preview-muted {
color: #94a3b8;
font-style: italic;
}
.preview-mono {
font-family: monospace;
font-size: 0.88rem;
color: #334155;
}
// ── Status of Account (SOA) ───────────────────────────────────────────────────
.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;
}
// ── Dormancy Monitoring Timeline ──────────────────────────────────────────────
.preview-dormancy-timeline {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 1.25rem 0;
overflow-x: auto;
padding-bottom: 8px;
}
.preview-dorm-step {
display: flex;
flex-direction: column;
align-items: center;
flex: 0 0 140px;
gap: 10px;
&__dot {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid #e2e8f0;
background: #f8fafc;
position: relative;
box-shadow: 0 0 0 4px #fff;
z-index: 2;
&--inactive {
border-color: #f59e0b;
background: #fffbeb;
&::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #f59e0b;
}
}
&--dormant {
border-color: #ef4444;
background: #fef2f2;
&::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #ef4444;
}
}
&--escheat {
border-color: #7c3aed;
background: #f5f3ff;
&::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #7c3aed;
}
}
}
&__body {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 2px;
}
&__label {
font-size: 12px;
font-weight: 700;
color: #1e293b;
}
&__days {
font-size: 10.5px;
color: #64748b;
line-height: 1.3;
}
}
.preview-dorm-connector {
flex: 0 0 40px;
height: 3px;
background: linear-gradient(90deg, #e2e8f0, #cbd5e1);
margin-top: 16px;
border-radius: 2px;
}
@@ -76,6 +76,8 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
@Input() fixedDepositsAccountProductTemplate: any;
/** Fixed Deposits Account Data */
@Input() fixedDepositAccountData: any;
/** Tenure selected from the interest rate chart step */
@Input() selectedTenure: { depositPeriod: number; depositPeriodFrequencyId: number } | null = null;
/** Charges Displayed Columns */
chargesDisplayedColumns: string[] = [
@@ -110,6 +112,28 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
/** Expand Chart Slab Index used in the view */
expandChartSlabIndex: number;
readonly fdTypeLabels: Record<string, string> = {
SINGLE: 'FD Single (Days < 1 Year)',
DOUBLE: 'FD Double (≥ 1 Year)',
SPECIAL: 'FD Special (Monthly Interest Payout)'
};
getFdTypeLabel(fdType: string): string {
return this.fdTypeLabels[fdType] || fdType;
}
get currencyName(): string {
return this.fixedDepositsAccountProductTemplate?.currency?.name || '—';
}
get decimalPlaces(): string | number {
return this.fixedDepositsAccountProductTemplate?.currency?.decimalPlaces ?? '—';
}
get inMultiplesOf(): string | number {
return this.fixedDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—';
}
/** Form submission event */
@Output() submitEvent = new EventEmitter();
@@ -117,7 +141,34 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
ngOnChanges() {
if (this.fixedDepositsAccountProductTemplate) {
this.interestRateChartData = this.fixedDepositsAccountProductTemplate.accountChart.chartSlabs;
this.interestRateChartData = this.fixedDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
}
}
get matchedSlabIndex(): number {
if (!this.selectedTenure || !this.interestRateChartData?.length) return -1;
const { depositPeriod, depositPeriodFrequencyId } = this.selectedTenure;
// Convert selected tenure to days for comparison
const toDays = (v: number, id: number) => {
if (id === 2) return v * 30; // Months
if (id === 3) return v * 365; // Years
return v; // Days (id=0)
};
const enteredDays = toDays(depositPeriod, depositPeriodFrequencyId);
return this.interestRateChartData.findIndex((slab: any) => {
const periodTypeValue = (slab.periodType?.value || '').toLowerCase();
const slabId = periodTypeValue.includes('month') ? 2 : periodTypeValue.includes('year') ? 3 : 0;
const fromDays = toDays(slab.fromPeriod ?? 0, slabId);
const toDaysVal = slab.toPeriod != null ? toDays(slab.toPeriod, slabId) : Infinity;
return enteredDays >= fromDays && enteredDays <= toDaysVal;
});
}
get selectedTenureLabel(): string {
if (!this.selectedTenure) return '';
const { depositPeriod, depositPeriodFrequencyId } = this.selectedTenure;
const unitMap: Record<number, string> = { 0: 'Days', 2: 'Months', 3: 'Years' };
return `${depositPeriod} ${unitMap[depositPeriodFrequencyId] ?? ''}`;
}
}
+2 -3
View File
@@ -5,13 +5,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
/* tslint:disable */
export default {
'mifos_x': {
'version': '260504',
'hash': 'e9d2f4b5'
'version': '260507',
'hash': 'b0cffd38'
},
'allow_switching_backend_instance': true
};