Files
cbsnexora/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html
T
niteshandClaude Opus 5.5 3e5cc26e49 feat(fixed-deposits): FD quote from the account engine, FD Special payout schedule popup, Special terms locked
- Rate chart step (account opening): the rate, maturity date, maturity amount
  and interest now come from POST fixeddepositaccounts/account-preview for
  every FD type, so the quote matches the account once created (the old
  calculate-fd-interest call counted quarters from the deposit date and was
  off, e.g. FD DOUBLE by 36,979.23). The client-side formula fallback is
  removed: on an error the message is shown, nothing is estimated.
- Dates are sent as the local calendar day (toISOString() turned 21 Oct at
  local midnight into 20 Oct).
- FD Special: total interest paid out monthly, monthly payout, principal at
  maturity, maturity date, number of payouts, and "View payout schedule",
  which opens the existing Monthly Interest Payout Schedule dialog in a new
  preview mode (Days column, no paid / pending status).
- Preview step: FD Special labels (total interest paid out monthly,
  principal at maturity).
- FD product terms: choosing FD Special locks Interest Calculation Type to
  Simple, as Compounding and Posting are locked to Monthly (the backend
  refuses anything else).
- Preview step: three existing `!= null` template checks rewritten for the
  eqeqeq lint rule, same meaning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:44:47 +05:30

599 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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="fdrc-wrapper">
<!-- ══ Chart Header Card ════════════════════════════════════════════════ -->
@if (interestRateChartData.length > 0) {
<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>
<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">
<span class="fdrc-pill" [class.fdrc-pill--active]="isActive" [class.fdrc-pill--inactive]="!isActive">
<i class="fas" [class.fa-check-circle]="isActive" [class.fa-times-circle]="!isActive"></i>
{{ isActive ? 'Active' : 'Inactive' }}
</span>
@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>
}
<!-- ══ 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>
}
<!-- ══ 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 (all FD types) -->
<div class="fdrc-calc-field">
<label class="fdrc-calc-label">
Deposit Amount
<span class="fdrc-calc-optional">(optional — to calculate maturity amount)</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>
<!-- Senior Citizen -->
<div class="fdrc-calc-field fdrc-calc-field--senior">
<label class="fdrc-senior-label">
<input type="checkbox" [ngModel]="isSeniorCitizen" [disabled]="true" />
<span class="fdrc-senior-text">
<i class="fas fa-user-shield"></i>
Senior Citizen
</span>
<span class="fdrc-senior-hint">{{ seniorCitizenHint }}</span>
</label>
</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__body">
<!-- 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>
<!-- Senior Citizen Status — always shown when checked -->
@if (isSeniorCitizen) {
@if ((matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<div class="fdrc-sc-status fdrc-sc-status--applied">
<i class="fas fa-user-shield fdrc-sc-status__icon"></i>
<span class="fdrc-sc-status__text">Senior Citizen Rate Applied</span>
<span class="fdrc-sc-status__bonus">
+{{
(matchResult.slab.seniorCitizenRate ?? 0) - (matchResult.slab.annualInterestRate ?? 0)
| number: '1.2-2'
}}% bonus
</span>
</div>
} @else {
<div class="fdrc-sc-status fdrc-sc-status--notice">
<i class="fas fa-info-circle fdrc-sc-status__icon"></i>
<span class="fdrc-sc-status__text"
>Senior Citizen selected — no additional rate configured for this slab, normal rate applied</span
>
</div>
}
}
<!-- Rate Display -->
@if (isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<!-- Before / After comparison -->
<div class="fdrc-rate-compare">
<div class="fdrc-rate-compare__block">
<span class="fdrc-rate-compare__tag">Normal Rate</span>
<span class="fdrc-rate-compare__val fdrc-rate-compare__val--strike"
>{{ matchResult.slab.annualInterestRate | number: '1.2-2' }}%</span
>
<span class="fdrc-rate-compare__sub">p.a.</span>
</div>
<div class="fdrc-rate-compare__arrow"><i class="fas fa-long-arrow-alt-right"></i></div>
<div class="fdrc-rate-compare__block fdrc-rate-compare__block--sc">
<span class="fdrc-rate-compare__tag fdrc-rate-compare__tag--sc"
><i class="fas fa-user-shield"></i> Sr. Citizen Rate</span
>
<span class="fdrc-rate-compare__val fdrc-rate-compare__val--sc"
>{{ matchResult.appliedRate | number: '1.2-2' }}%</span
>
<span class="fdrc-rate-compare__sub">p.a.</span>
</div>
</div>
} @else {
<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 p.a.</span>
</div>
}
<!-- Slab Info -->
<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>
<!-- Maturity Summary -->
@if (matchResult.maturityAmount) {
<div class="fdrc-maturity-card">
<div class="fdrc-maturity-card__header">
<i class="fas fa-coins"></i>
<span>Maturity Summary</span>
@if (isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<span class="fdrc-sc-tag"><i class="fas fa-user-shield"></i> Sr. Citizen Rate</span>
}
</div>
<div class="fdrc-maturity-card__body">
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Deposit Amount</span>
<span class="fdrc-maturity-row__value">₹ {{ principalAmount | number: '1.2-2' }}</span>
</div>
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Tenure</span>
<span class="fdrc-maturity-row__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
</div>
<div
class="fdrc-maturity-row"
[class.fdrc-maturity-row--sc]="isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0"
>
<span class="fdrc-maturity-row__label">
@if (isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<i class="fas fa-user-shield"></i> Sr. Citizen Rate
} @else {
Interest Rate
}
</span>
<span class="fdrc-maturity-row__value">{{ matchResult.appliedRate | number: '1.2-2' }}% p.a.</span>
</div>
<div class="fdrc-maturity-row fdrc-maturity-row--interest">
<span class="fdrc-maturity-row__label">{{
isSpecial ? 'Total Interest (paid out monthly)' : 'Interest Earned'
}}</span>
<span class="fdrc-maturity-row__value">₹ {{ matchResult.interestEarned | number: '1.2-2' }}</span>
</div>
@if (isSpecial && matchResult.estimatedMonthlyPayout) {
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Monthly Payout (approx., by days in the month)</span>
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--highlight">
₹ {{ matchResult.estimatedMonthlyPayout | number: '1.2-2' }} / month
</span>
</div>
}
<div class="fdrc-maturity-row fdrc-maturity-row--total">
<span class="fdrc-maturity-row__label">
@if (isSpecial) {
Principal at Maturity
} @else {
Maturity Amount
}
</span>
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--big">
₹ {{ matchResult.maturityAmount | number: '1.2-2' }}
</span>
</div>
@if (isSpecial && specialPreview) {
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Maturity Date</span>
<span class="fdrc-maturity-row__value">{{
specialPreview.maturityDate | date: 'dd MMM yyyy'
}}</span>
</div>
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Monthly payouts</span>
<span class="fdrc-maturity-row__value">{{ specialPreview.numberOfPayouts }}</span>
</div>
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Principal + all payouts</span>
<span class="fdrc-maturity-row__value"
>₹ {{ specialPreview.totalValueAtMaturity | number: '1.2-2' }}</span
>
</div>
}
</div>
</div>
@if (isSpecial && specialPreview) {
<div class="fdrc-schedule">
<div class="fdrc-schedule__head">
<span><i class="fas fa-calendar-alt"></i> Monthly Interest Payout Schedule</span>
<button
mat-stroked-button
type="button"
class="fdrc-schedule__toggle"
(click)="openSpecialSchedule()"
>
View payout schedule ({{ specialPreview.numberOfPayouts }})
</button>
</div>
<div class="fdrc-schedule__note">
From {{ specialPreview.startDate | date: 'dd MMM yyyy' }} (the day the account is opened) to
{{ specialPreview.maturityDate | date: 'dd MMM yyyy' }}, worked out by the account engine from the
product.
</div>
</div>
}
}
@if (specialPreviewLoading) {
<div class="fdrc-payout-hint">
<i class="fas fa-spinner fa-spin"></i>
Working out the maturity from the product…
</div>
}
@if (specialPreviewError) {
<div class="fdrc-payout-hint fdrc-payout-hint--error">
<i class="fas fa-exclamation-triangle"></i>
{{ specialPreviewError }}
</div>
}
@if (!matchResult.maturityAmount && !specialPreviewLoading && !specialPreviewError) {
<div class="fdrc-payout-hint">
<i class="fas fa-lightbulb"></i>
Enter a deposit amount above to see the maturity calculation.
</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>
<!-- Senior Citizen Rate Column -->
<ng-container matColumnDef="seniorCitizenRate">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">
<i class="fas fa-user-shield" style="margin-right: 4px; color: #6366f1"></i>Sr. Citizen Rate
</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
@if (slab.seniorCitizenRate > 0) {
<span class="fdrc-rate-badge fdrc-rate-badge--senior"
>{{ slab.seniorCitizenRate | number: '1.2-2' }}%</span
>
} @else {
<span class="fdrc-td--na">—</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>