Four independent, self-contained changes bundled from this session's work:
1. Auto-derive Senior Citizen / Minor / Customer Segment instead of manual entry
Senior Citizen was a manual, staff-editable checkbox on FD/RD account
creation with no validation against the client's actual age -- this let
non-senior clients get the senior-citizen interest rate by mistake (found
twice on live test accounts, where m_savings_account.is_senior_citizen
was true despite the client being confirmed non-senior at the client
record level). Likewise "minor" client classification was hardcoded to
the age-18 threshold with no single source of truth, and FD/RD account
creation's "Customer Segment" (Individual / Non-Individual) had no
connection to the Individual/Non-Individual Legal Form already captured
at client creation.
- New src/app/clients/shared/client-age.util.ts centralises age/DOB
derivation: MINOR_AGE_THRESHOLD (now 10, was 18), SENIOR_CITIZEN_AGE_
THRESHOLD (60), AGE_OF_MAJORITY (18, kept separate from the minor
threshold -- majority date math is unrelated to the minor/adult
classification threshold), calculateAge/isMinorAge/isSeniorCitizenAge/
isIndividualClient/deriveSeniorCitizenStatus.
- FD/RD interest-rate-chart steps: Senior Citizen checkbox is now derived
from the client's DOB (age >= 60, individual clients only) and
disabled for manual editing; a hint explains why it is checked/
unchecked.
- Client general step and all four account-details-steps (savings,
current, FD, RD): replaced hardcoded 18/60 literals with the shared
helpers; "Must be below 18" hints now read the actual threshold.
- FD/RD account-details-steps: Customer Segment auto-selects to match
the client's Legal Form (exact match, not substring -- a substring
match would misclassify "Individual" as matching "Non-Individual").
- RD preview step: fixed a pre-existing display bug that always showed
the product's default Customer Segment instead of what was actually
selected on the account.
2. Fix wrong Account Name / Account Type on Disbursement Review screens
Clerk disbursement review and "My Disbursement Requests" showed the
account NUMBER instead of the holder's name, and always labelled the
account type "SAVINGS" regardless of the real account type (FD/RD/
Share/Loan), because transaction-entry.component.ts's account-type
mapping only checked for CA/CB and its account-name fallback chain
never tried clientName. Added creditEntryAccountType() covering
GL/CA/CB/FD/RD/SHR/LA, and accountTypeLabel() display helpers on both
review screens.
3. Stop reversed transactions from distorting displayed running balances
Passbook and Transactions tabs were including reversed transactions in
their client-side running-balance recalculation, so a reversed posting
(visible struck-through) still shifted the balance shown for every
transaction after it. Both now filter out txn.reversed before computing
running balances; Transactions tab also defaults "Hide Reversed" to on.
4. New Interest Schedule dialog on the FD account view
Added custom-dialogs/interest-schedule-dialog, opened from the FD
General tab, showing the account's period-by-period interest schedule.
Excludes src/environments/.env.ts (auto-generated build version/hash
stamp, regenerated on every build -- not meaningful to hand-commit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
455 lines
20 KiB
HTML
455 lines
20 KiB
HTML
<!--
|
||
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 || 'RD 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>
|
||
<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>
|
||
}
|
||
|
||
<!-- ══ 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">RD Rate Calculator</span>
|
||
<span class="fdrc-calc-card__sub">Enter your RD 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 -->
|
||
<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>
|
||
</div>
|
||
|
||
<!-- Principal Amount (Monthly Installment) -->
|
||
<div class="fdrc-calc-field">
|
||
<label class="fdrc-calc-label">
|
||
Monthly Installment
|
||
<span class="fdrc-calc-optional">(optional — for maturity estimation)</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. 5000"
|
||
/>
|
||
</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 RD 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>
|
||
}
|
||
</div>
|
||
|
||
<!-- Maturity Summary -->
|
||
@if (matchResult.maturityAmount) {
|
||
<div class="fdrc-maturity-card">
|
||
<div class="fdrc-maturity-card__header">
|
||
<i class="fas fa-coins"></i>
|
||
<span>RD Maturity Summary (Estimate)</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">Monthly Installment</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">Estimated Interest Earned</span>
|
||
<span class="fdrc-maturity-row__value">₹ {{ matchResult.interestEarned | number: '1.2-2' }}</span>
|
||
</div>
|
||
<div class="fdrc-maturity-row fdrc-maturity-row--total">
|
||
<span class="fdrc-maturity-row__label">Estimated Maturity Amount</span>
|
||
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--big"
|
||
>₹ {{ matchResult.maturityAmount | number: '1.2-2' }}</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
@if (!matchResult.maturityAmount) {
|
||
<div class="fdrc-payout-hint">
|
||
<i class="fas fa-lightbulb"></i>
|
||
Enter a monthly installment 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"
|
||
>
|
||
<fa-icon [icon]="expandChartSlabIndex !== i ? 'eye' : 'eye-slash'"></fa-icon>
|
||
{{ 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>
|