feat: senior citizen rate fix, joint ownership cap, FD/RD UI improvements
- Senior citizen interest rate correctly sent in FD/RD create payload (isSeniorCitizen, seniorCitizenInterestRate, nominalAnnualInterestRate override) - Joint account ownership capped at 100%: Add Holder/Partner button disabled when 100% reached; dialog enforces remaining share as max across all account types - FD/RD interest rate chart step: rate calculator with tenure, senior citizen toggle, maturity preview - FD/RD preview step improvements - Base FD interest rate chart component added under fixed-deposit-products - Base recurring deposit interest rate chart component removed (replaced) - Savings account details step: joint holder improvements - Products routing and module updated for base FD rate chart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
846b2484d7
commit
61b0244fa0
+22
-4
@@ -106,20 +106,38 @@
|
||||
<div class="arc-step-label arc-step-label--mt">
|
||||
<span class="arc-step-badge">2</span>
|
||||
<span>Ownership Share</span>
|
||||
@if (maxShare < 100) {
|
||||
<span class="arc-remaining-badge">Max available: {{ maxShare }}%</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="arc-share-row">
|
||||
<mat-form-field appearance="outline" class="arc-share-field">
|
||||
<mat-label>Share Percentage</mat-label>
|
||||
<input matInput type="number" formControlName="sharePercentage" min="1" max="100" />
|
||||
<input matInput type="number" formControlName="sharePercentage" min="1" [max]="maxShare" />
|
||||
<span matSuffix class="arc-percent-suffix">%</span>
|
||||
<mat-error>Enter a value between 1 and 100</mat-error>
|
||||
@if (form.get('sharePercentage').hasError('max')) {
|
||||
<mat-error>Maximum available is {{ maxShare }}%</mat-error>
|
||||
} @else {
|
||||
<mat-error>Enter a value between 1 and {{ maxShare }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="arc-share-hints">
|
||||
@for (hint of [25, 33, 50, 100]; track hint) {
|
||||
<button type="button" class="arc-hint-chip" (click)="form.get('sharePercentage').setValue(hint)">
|
||||
{{ hint }}%
|
||||
@if (hint <= maxShare) {
|
||||
<button type="button" class="arc-hint-chip" (click)="form.get('sharePercentage').setValue(hint)">
|
||||
{{ hint }}%
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (maxShare > 0 && ![25, 33, 50, 100].includes(maxShare)) {
|
||||
<button
|
||||
type="button"
|
||||
class="arc-hint-chip arc-hint-chip--remaining"
|
||||
(click)="form.get('sharePercentage').setValue(maxShare)"
|
||||
>
|
||||
{{ maxShare }}%
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
+25
@@ -102,6 +102,19 @@ mifosx-add-related-client-dialog {
|
||||
}
|
||||
}
|
||||
|
||||
.arc-remaining-badge {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border: 1px solid #fcd34d;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.arc-step-badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -350,6 +363,18 @@ mifosx-add-related-client-dialog {
|
||||
background: $primary-light;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&--remaining {
|
||||
border-color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
|
||||
&:hover {
|
||||
border-color: #d97706;
|
||||
background: #fef3c7;
|
||||
color: #78350f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────────
|
||||
|
||||
+8
-3
@@ -32,16 +32,21 @@ export class AddRelatedClientDialogComponent implements OnDestroy {
|
||||
private fb = inject(UntypedFormBuilder);
|
||||
private clientsService = inject(ClientsService);
|
||||
dialogRef = inject(MatDialogRef<AddRelatedClientDialogComponent>);
|
||||
data: { excludedClientIds?: number[] } = inject(MAT_DIALOG_DATA, { optional: true }) ?? {};
|
||||
data: { excludedClientIds?: number[]; maxSharePercentage?: number } =
|
||||
inject(MAT_DIALOG_DATA, { optional: true }) ?? {};
|
||||
|
||||
get maxShare(): number {
|
||||
return this.data?.maxSharePercentage ?? 100;
|
||||
}
|
||||
|
||||
form: UntypedFormGroup = this.fb.group({
|
||||
searchText: [''],
|
||||
sharePercentage: [
|
||||
null,
|
||||
this.data?.maxSharePercentage ?? null,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(100)
|
||||
Validators.max(this.data?.maxSharePercentage ?? 100)
|
||||
]
|
||||
],
|
||||
signingAuthority: [null],
|
||||
|
||||
+2
@@ -259,6 +259,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="soa-add-btn"
|
||||
[disabled]="totalJointSharePercentage >= 100"
|
||||
(click)="openAddJointHolderDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> Add Holder
|
||||
@@ -761,6 +762,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="ni-add-btn"
|
||||
[disabled]="totalSharePercentage >= 100"
|
||||
(click)="openAddRelatedClientDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> Add Partner
|
||||
|
||||
+4
-2
@@ -364,11 +364,12 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
...(this.primaryClientId ? [this.primaryClientId] : []),
|
||||
...this.jointHolders.map((h: any) => h.clientId).filter(Boolean)
|
||||
];
|
||||
const maxSharePercentage = Math.max(0, 100 - this.totalJointSharePercentage);
|
||||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||||
width: '500px',
|
||||
panelClass: 'arc-dialog-panel',
|
||||
disableClose: true,
|
||||
data: { excludedClientIds }
|
||||
data: { excludedClientIds, maxSharePercentage }
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
@@ -443,11 +444,12 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
...(this.primaryClientId ? [this.primaryClientId] : []),
|
||||
...this.relatedClients.map((c: any) => c.clientId).filter(Boolean)
|
||||
];
|
||||
const maxSharePercentage = Math.max(0, 100 - this.totalSharePercentage);
|
||||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||||
width: '500px',
|
||||
panelClass: 'arc-dialog-panel',
|
||||
disableClose: true,
|
||||
data: { excludedClientIds }
|
||||
data: { excludedClientIds, maxSharePercentage }
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
|
||||
+1
@@ -74,6 +74,7 @@
|
||||
<mifosx-fixed-deposit-account-interest-rate-chart-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[cbsBaseRates]="cbsBaseRates"
|
||||
(tenureSelected)="onTenureSelected($event)"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-interest-rate-chart-step>
|
||||
|
||||
+22
@@ -14,6 +14,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FixedDepositsService } from '../fixed-deposits.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
|
||||
/** Custom Components */
|
||||
import { FixedDepositAccountDetailsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component';
|
||||
@@ -61,6 +62,7 @@ export class CreateFixedDepositAccountComponent {
|
||||
private fixedDepositsService = inject(FixedDepositsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private alertService = inject(AlertService);
|
||||
private productsService = inject(ProductsService);
|
||||
|
||||
@ViewChild(FixedDepositAccountDetailsStepComponent, { static: true })
|
||||
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
|
||||
@@ -71,6 +73,8 @@ export class CreateFixedDepositAccountComponent {
|
||||
fixedDepositsAccountTemplate: any;
|
||||
/** Fixed Deposit Account Product Template */
|
||||
fixedDepositsAccountProductTemplate: any;
|
||||
/** Active CBS base interest rate versions for the selected product's FD type */
|
||||
cbsBaseRates: any[] = [];
|
||||
/** Tenure selected from the interest rate chart step */
|
||||
selectedTenure: {
|
||||
depositPeriod: number;
|
||||
@@ -79,6 +83,7 @@ export class CreateFixedDepositAccountComponent {
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
@@ -101,6 +106,11 @@ export class CreateFixedDepositAccountComponent {
|
||||
*/
|
||||
setTemplate($event: any) {
|
||||
this.fixedDepositsAccountProductTemplate = $event;
|
||||
const fdType = $event?.fdType;
|
||||
this.productsService.getFdBaseInterestRates(fdType).subscribe({
|
||||
next: (rates: any[]) => (this.cbsBaseRates = rates || []),
|
||||
error: () => (this.cbsBaseRates = [])
|
||||
});
|
||||
}
|
||||
|
||||
onTenureSelected($event: {
|
||||
@@ -110,6 +120,7 @@ export class CreateFixedDepositAccountComponent {
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
}) {
|
||||
this.selectedTenure = $event;
|
||||
}
|
||||
@@ -257,6 +268,17 @@ export class CreateFixedDepositAccountComponent {
|
||||
}));
|
||||
}
|
||||
|
||||
// Senior citizen rate override
|
||||
if (
|
||||
this.selectedTenure?.isSeniorCitizen &&
|
||||
this.selectedTenure.appliedRate != null &&
|
||||
this.selectedTenure.appliedRate > 0
|
||||
) {
|
||||
fixedDepositAccount.isSeniorCitizen = true;
|
||||
fixedDepositAccount.seniorCitizenInterestRate = this.selectedTenure.appliedRate;
|
||||
fixedDepositAccount.nominalAnnualInterestRate = this.selectedTenure.appliedRate;
|
||||
}
|
||||
|
||||
this.fixedDepositsService.createFixedDepositAccount(fixedDepositAccount).subscribe({
|
||||
next: (response: any) => {
|
||||
this.router.navigate(
|
||||
|
||||
+1
@@ -370,6 +370,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="soa-add-btn"
|
||||
[disabled]="totalJointSharePercentage >= 100"
|
||||
(click)="openAddJointHolderDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> Add Holder
|
||||
|
||||
+2
-1
@@ -473,11 +473,12 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
|
||||
...(this.primaryClientId ? [this.primaryClientId] : []),
|
||||
...this.jointHolders.map((h: any) => h.clientId).filter(Boolean)
|
||||
];
|
||||
const maxSharePercentage = Math.max(0, 100 - this.totalJointSharePercentage);
|
||||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||||
width: '500px',
|
||||
panelClass: 'arc-dialog-panel',
|
||||
disableClose: true,
|
||||
data: { excludedClientIds }
|
||||
data: { excludedClientIds, maxSharePercentage }
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
|
||||
+108
-22
@@ -8,7 +8,7 @@
|
||||
|
||||
<div class="fdrc-wrapper">
|
||||
<!-- ══ Chart Header Card ════════════════════════════════════════════════ -->
|
||||
@if (fixedDepositsAccountProductTemplate?.accountChart) {
|
||||
@if (interestRateChartData.length > 0) {
|
||||
<div class="fdrc-header-card">
|
||||
<div class="fdrc-header-card__left">
|
||||
<div class="fdrc-header-card__icon">
|
||||
@@ -163,6 +163,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Senior Citizen -->
|
||||
<div class="fdrc-calc-field fdrc-calc-field--senior">
|
||||
<label class="fdrc-senior-label">
|
||||
<input type="checkbox" [(ngModel)]="isSeniorCitizen" (change)="onSeniorCitizenChange()" />
|
||||
<span class="fdrc-senior-text">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
Senior Citizen
|
||||
</span>
|
||||
<span class="fdrc-senior-hint">Higher rate applied if available</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
|
||||
@@ -186,34 +198,80 @@
|
||||
<!-- 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 -->
|
||||
<!-- 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>
|
||||
<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>
|
||||
<!-- 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>
|
||||
<strong
|
||||
>{{ matchResult.slab.fromPeriod }}–{{ matchResult.slab.toPeriod ?? '∞' }}
|
||||
{{ matchResult.slab.periodType?.value }}</strong
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
@if (matchResult.slab.description) {
|
||||
<div class="fdrc-result-meta__item">
|
||||
@@ -230,18 +288,21 @@
|
||||
>{{ matchResult.slab.amountRangeFrom | number }}–{{
|
||||
matchResult.slab.amountRangeTo | number
|
||||
}}</strong
|
||||
>
|
||||
</span>
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Maturity Summary (all FD types) -->
|
||||
@if (matchResult.maturityAmount !== null) {
|
||||
<!-- 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">
|
||||
@@ -252,15 +313,24 @@
|
||||
<span class="fdrc-maturity-row__label">Tenure</span>
|
||||
<span class="fdrc-maturity-row__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
|
||||
</div>
|
||||
<div class="fdrc-maturity-row">
|
||||
<span class="fdrc-maturity-row__label">Interest Rate</span>
|
||||
<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">Interest Earned</span>
|
||||
<span class="fdrc-maturity-row__value">₹ {{ matchResult.interestEarned | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (isSpecial && matchResult.estimatedMonthlyPayout !== null) {
|
||||
@if (isSpecial && matchResult.estimatedMonthlyPayout) {
|
||||
<div class="fdrc-maturity-row">
|
||||
<span class="fdrc-maturity-row__label">Monthly Payout</span>
|
||||
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--highlight">
|
||||
@@ -350,6 +420,22 @@
|
||||
</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>
|
||||
|
||||
+284
-12
@@ -387,8 +387,7 @@ $red-light: #fee2e2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 22px;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.fdrc-calc-btn {
|
||||
@@ -515,25 +514,153 @@ $red-light: #fee2e2;
|
||||
// ── 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Senior Citizen Status Banner ──────────────────────────────────────────
|
||||
|
||||
.fdrc-sc-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
&--applied {
|
||||
background: #ede9fe;
|
||||
border: 1.5px solid #a78bfa;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
&__body {
|
||||
&--notice {
|
||||
background: #fef3c7;
|
||||
border: 1.5px solid #fcd34d;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__bonus {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rate Comparison Block (Normal → Senior) ───────────────────────────────
|
||||
|
||||
.fdrc-rate-compare {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: $gray-200;
|
||||
color: $gray-600;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
|
||||
&--sc {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
}
|
||||
}
|
||||
|
||||
&__val {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
|
||||
&--strike {
|
||||
color: $gray-600;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&--sc {
|
||||
color: #5b21b6;
|
||||
}
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: $gray-600;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: 22px;
|
||||
color: $gray-600;
|
||||
padding-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── SC Tag (maturity card header) ─────────────────────────────────────────
|
||||
|
||||
.fdrc-sc-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
padding: 3px 10px;
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// ── SC Maturity Row ───────────────────────────────────────────────────────
|
||||
|
||||
.fdrc-maturity-row--sc {
|
||||
.fdrc-maturity-row__label {
|
||||
color: #5b21b6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fdrc-maturity-row__value {
|
||||
color: #5b21b6;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,6 +881,151 @@ $red-light: #fee2e2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fdrc-rate-badge--senior {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
}
|
||||
|
||||
// ── Senior Citizen Rate Comparison View ───────────────────────────────────
|
||||
|
||||
.fdrc-senior-rate-view {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__normal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
&__senior {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: $gray-200;
|
||||
color: $gray-600;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
|
||||
&--sr {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
}
|
||||
}
|
||||
|
||||
&__strike {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: $gray-600;
|
||||
text-decoration: line-through;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__applied {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #5b21b6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: 18px;
|
||||
color: $gray-600;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__bonus {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 14px;
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
i {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fdrc-senior-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fdrc-calc-field--senior {
|
||||
flex: 0 0 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fdrc-senior-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #6366f1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrc-senior-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-weight: 600;
|
||||
color: #4338ca;
|
||||
font-size: 13px;
|
||||
|
||||
i {
|
||||
color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrc-senior-hint {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.fdrc-incentive-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
+33
-6
@@ -71,6 +71,8 @@ export interface SlabMatchResult {
|
||||
export class FixedDepositAccountInterestRateChartStepComponent implements OnChanges {
|
||||
@Input() fixedDepositsAccountTemplate: any;
|
||||
@Input() fixedDepositsAccountProductTemplate: any;
|
||||
/** Active CBS base interest rate versions — when provided, overrides the product chart slabs */
|
||||
@Input() cbsBaseRates: any[] = [];
|
||||
|
||||
@Output() tenureSelected = new EventEmitter<{
|
||||
depositPeriod: number;
|
||||
@@ -79,6 +81,7 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
}>();
|
||||
|
||||
interestRateChartData: any[] = [];
|
||||
@@ -87,9 +90,12 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
'period',
|
||||
'amountRange',
|
||||
'interest',
|
||||
'seniorCitizenRate',
|
||||
'description',
|
||||
'actions'
|
||||
];
|
||||
|
||||
isSeniorCitizen = false;
|
||||
incentivesDisplayedColumns: string[] = [
|
||||
'entityType',
|
||||
'attributeName',
|
||||
@@ -151,17 +157,26 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.fixedDepositsAccountProductTemplate) {
|
||||
this.interestRateChartData = this.fixedDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
|
||||
const fdType = this.fixedDepositsAccountProductTemplate?.fdType;
|
||||
const activeBase = (this.cbsBaseRates || []).find((r) => r.fdType === fdType && r.isActive);
|
||||
this.interestRateChartData = activeBase?.slabs
|
||||
? this.normalizeCbsSlabs(activeBase.slabs)
|
||||
: (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];
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeCbsSlabs(slabs: any[]): any[] {
|
||||
return (slabs || []).map((s) => ({
|
||||
...s,
|
||||
periodType: { id: s.periodTypeEnum, value: s.periodTypeLabel }
|
||||
}));
|
||||
}
|
||||
|
||||
// Convert entered tenure to Days for slab matching
|
||||
private toDays(value: number, unit: FdPeriodUnit): number {
|
||||
if (unit === 'Days') return value;
|
||||
@@ -229,7 +244,10 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
return;
|
||||
}
|
||||
|
||||
const rate = matchedSlab.annualInterestRate ?? 0;
|
||||
const rate =
|
||||
this.isSeniorCitizen && (matchedSlab.seniorCitizenRate ?? 0) > 0
|
||||
? (matchedSlab.seniorCitizenRate ?? 0)
|
||||
: (matchedSlab.annualInterestRate ?? 0);
|
||||
const result: SlabMatchResult = {
|
||||
slab: matchedSlab,
|
||||
slabIndex: matchedIndex,
|
||||
@@ -266,7 +284,8 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
depositAmount: this.principalAmount || undefined,
|
||||
maturityAmount: result.maturityAmount,
|
||||
interestEarned: result.interestEarned,
|
||||
appliedRate: result.appliedRate
|
||||
appliedRate: result.appliedRate,
|
||||
isSeniorCitizen: this.isSeniorCitizen
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -294,12 +313,20 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
this.validationWarning = null;
|
||||
}
|
||||
|
||||
onSeniorCitizenChange() {
|
||||
if (this.tenureValue && this.tenureValue > 0) {
|
||||
this.calculateRate();
|
||||
} else {
|
||||
this.matchResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
isMatchedSlab(index: number): boolean {
|
||||
return this.matchResult?.slabIndex === index;
|
||||
}
|
||||
|
||||
get chartName(): string {
|
||||
return this.fixedDepositsAccountProductTemplate?.accountChart?.name || '';
|
||||
return this.fixedDepositsAccountProductTemplate?.accountChart?.name || 'CBS Base Interest Rate';
|
||||
}
|
||||
|
||||
get chartFromDate(): any {
|
||||
|
||||
+31
-4
@@ -488,7 +488,26 @@
|
||||
<div class="preview-body">
|
||||
<!-- Selected Tenure + Rate Banner -->
|
||||
@if (selectedTenure && matchedSlabIndex >= 0) {
|
||||
<div class="preview-tenure-banner">
|
||||
<!-- Senior Citizen Status Banner -->
|
||||
@if (isSeniorCitizenSelected) {
|
||||
@if (seniorCitizenBonus > 0) {
|
||||
<div class="preview-sc-status preview-sc-status--applied">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Senior Citizen Rate Applied</span>
|
||||
<span class="preview-sc-status__bonus">+{{ seniorCitizenBonus | formatNumber }}% bonus</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="preview-sc-status preview-sc-status--notice">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>Senior Citizen selected — no additional rate configured for this slab</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
class="preview-tenure-banner"
|
||||
[class.preview-tenure-banner--sc]="isSeniorCitizenSelected && seniorCitizenBonus > 0"
|
||||
>
|
||||
<div class="preview-tenure-banner__left">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<div>
|
||||
@@ -498,9 +517,17 @@
|
||||
</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
|
||||
<span class="preview-tenure-banner__rate-label">
|
||||
@if (isSeniorCitizenSelected && seniorCitizenBonus > 0) {
|
||||
<i class="fas fa-user-shield" style="color: #5b21b6; margin-right: 4px"></i>Sr. Citizen Rate
|
||||
} @else {
|
||||
Applicable Interest Rate
|
||||
}
|
||||
</span>
|
||||
<span
|
||||
class="preview-tenure-banner__rate"
|
||||
[style.color]="isSeniorCitizenSelected && seniorCitizenBonus > 0 ? '#5b21b6' : ''"
|
||||
>{{ appliedInterestRate | formatNumber }}%</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-tenure-banner__slab">
|
||||
|
||||
+48
@@ -253,6 +253,54 @@
|
||||
border-left: 3px solid #16a34a;
|
||||
}
|
||||
|
||||
// ── Senior Citizen Status Banner (preview) ────────────────────────────────────
|
||||
.preview-sc-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
&--applied {
|
||||
background: #ede9fe;
|
||||
border: 1.5px solid #a78bfa;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
&--notice {
|
||||
background: #fef3c7;
|
||||
border: 1.5px solid #fcd34d;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
&__bonus {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-tenure-banner--sc {
|
||||
border-color: #a78bfa;
|
||||
background: #f5f3ff;
|
||||
|
||||
.preview-tenure-banner__left > i {
|
||||
color: #5b21b6;
|
||||
}
|
||||
|
||||
.preview-tenure-banner__divider {
|
||||
background: #a78bfa;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Legend below table ────────────────────────────────────────────────────────
|
||||
.preview-slab-legend {
|
||||
display: flex;
|
||||
|
||||
+18
@@ -85,8 +85,26 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
} | null = null;
|
||||
|
||||
get appliedInterestRate(): number {
|
||||
if (this.selectedTenure?.appliedRate != null) return this.selectedTenure.appliedRate;
|
||||
if (this.matchedSlabIndex >= 0) return this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
get isSeniorCitizenSelected(): boolean {
|
||||
return !!this.selectedTenure?.isSeniorCitizen;
|
||||
}
|
||||
|
||||
get seniorCitizenBonus(): number {
|
||||
if (!this.isSeniorCitizenSelected || this.selectedTenure?.appliedRate == null) return 0;
|
||||
if (this.matchedSlabIndex < 0) return 0;
|
||||
const normalRate = this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate ?? 0;
|
||||
return this.selectedTenure.appliedRate - normalRate;
|
||||
}
|
||||
|
||||
/** Charges Displayed Columns */
|
||||
chargesDisplayedColumns: string[] = [
|
||||
'name',
|
||||
|
||||
+3
@@ -75,6 +75,7 @@
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[submittedOnDate]="submittedOnDate"
|
||||
[cbsBaseRates]="cbsBaseRates"
|
||||
(tenureSelected)="handleTenureSelected($event)"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-interest-rate-chart-step>
|
||||
@@ -105,6 +106,8 @@
|
||||
[rdMaturityAmount]="rdMaturityAmount"
|
||||
[rdInterestEarned]="rdInterestEarned"
|
||||
[rdDaysInYear]="rdDaysInYear"
|
||||
[rdAppliedRate]="rdAppliedRate"
|
||||
[rdIsSeniorCitizen]="rdIsSeniorCitizen"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-preview-step>
|
||||
|
||||
+17
@@ -77,10 +77,14 @@ export class CreateRecurringDepositsAccountComponent {
|
||||
recurringDepositsAccountTemplate: any;
|
||||
/** Recurring Deposit Account Product Template */
|
||||
recurringDepositsAccountProductTemplate: any;
|
||||
/** Active CBS base interest rate versions */
|
||||
cbsBaseRates: any[] = [];
|
||||
/** Selected tenure value and frequency from calculator */
|
||||
selectedTenureValue: number | null = null;
|
||||
selectedTenureFrequencyId: number | null = null;
|
||||
selectedDepositAmount: number | null = null;
|
||||
rdAppliedRate: number | null = null;
|
||||
rdIsSeniorCitizen = false;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => {
|
||||
@@ -94,6 +98,10 @@ export class CreateRecurringDepositsAccountComponent {
|
||||
*/
|
||||
setTemplate($event: any) {
|
||||
this.recurringDepositsAccountProductTemplate = $event;
|
||||
this.productsService.getFdBaseInterestRates().subscribe({
|
||||
next: (rates: any[]) => (this.cbsBaseRates = rates || []),
|
||||
error: () => (this.cbsBaseRates = [])
|
||||
});
|
||||
}
|
||||
|
||||
/** Captures tenure selection from calculator */
|
||||
@@ -101,6 +109,8 @@ export class CreateRecurringDepositsAccountComponent {
|
||||
this.selectedTenureValue = $event.depositPeriod;
|
||||
this.selectedTenureFrequencyId = $event.depositPeriodFrequencyId;
|
||||
this.selectedDepositAmount = $event.depositAmount;
|
||||
this.rdAppliedRate = $event.appliedRate ?? null;
|
||||
this.rdIsSeniorCitizen = $event.isSeniorCitizen ?? false;
|
||||
}
|
||||
|
||||
/** Get Recurring Deposit Account Details Form Data */
|
||||
@@ -283,6 +293,13 @@ export class CreateRecurringDepositsAccountComponent {
|
||||
recurringDepositAccount.mandatoryRecommendedDepositAmount = this.selectedDepositAmount;
|
||||
}
|
||||
|
||||
// Senior citizen rate override
|
||||
if (this.rdIsSeniorCitizen && this.rdAppliedRate != null && this.rdAppliedRate > 0) {
|
||||
recurringDepositAccount.isSeniorCitizen = true;
|
||||
recurringDepositAccount.seniorCitizenInterestRate = this.rdAppliedRate;
|
||||
recurringDepositAccount.nominalAnnualInterestRate = this.rdAppliedRate;
|
||||
}
|
||||
|
||||
const nominees = this.recurringDepositsAccountCurrencyStep?.nominees ?? [];
|
||||
if (nominees.length > 0) {
|
||||
recurringDepositAccount.nominees = nominees.map((n: any) => ({
|
||||
|
||||
+1
@@ -365,6 +365,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="soa-add-btn"
|
||||
[disabled]="totalJointSharePercentage >= 100"
|
||||
(click)="openAddJointHolderDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> Add Holder
|
||||
|
||||
+2
-1
@@ -434,11 +434,12 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
...(this.primaryClientId ? [this.primaryClientId] : []),
|
||||
...this.jointHolders.map((h: any) => h.clientId).filter(Boolean)
|
||||
];
|
||||
const maxSharePercentage = Math.max(0, 100 - this.totalJointSharePercentage);
|
||||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||||
width: '500px',
|
||||
panelClass: 'arc-dialog-panel',
|
||||
disableClose: true,
|
||||
data: { excludedClientIds }
|
||||
data: { excludedClientIds, maxSharePercentage }
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
|
||||
+106
-20
@@ -103,6 +103,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Senior Citizen -->
|
||||
<div class="fdrc-calc-field fdrc-calc-field--senior">
|
||||
<label class="fdrc-senior-label">
|
||||
<input type="checkbox" [(ngModel)]="isSeniorCitizen" (change)="onSeniorCitizenChange()" />
|
||||
<span class="fdrc-senior-text">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
Senior Citizen
|
||||
</span>
|
||||
<span class="fdrc-senior-hint">Higher rate applied if available</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
|
||||
@@ -126,34 +138,80 @@
|
||||
<!-- 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 -->
|
||||
<!-- 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>
|
||||
<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>
|
||||
<!-- 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>
|
||||
<strong
|
||||
>{{ matchResult.slab.fromPeriod }}–{{ matchResult.slab.toPeriod ?? '∞' }}
|
||||
{{ matchResult.slab.periodType?.value }}</strong
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
@if (matchResult.slab.description) {
|
||||
<div class="fdrc-result-meta__item">
|
||||
@@ -164,11 +222,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Maturity Summary -->
|
||||
@if (matchResult.maturityAmount !== null) {
|
||||
@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">
|
||||
@@ -179,8 +240,17 @@
|
||||
<span class="fdrc-maturity-row__label">Tenure</span>
|
||||
<span class="fdrc-maturity-row__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
|
||||
</div>
|
||||
<div class="fdrc-maturity-row">
|
||||
<span class="fdrc-maturity-row__label">Interest Rate</span>
|
||||
<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">
|
||||
@@ -189,9 +259,9 @@
|
||||
</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>
|
||||
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--big"
|
||||
>₹ {{ matchResult.maturityAmount | number: '1.2-2' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,6 +333,22 @@
|
||||
</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>
|
||||
|
||||
+288
-12
@@ -337,8 +337,7 @@ $red-light: #fee2e2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 22px;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.fdrc-calc-btn {
|
||||
@@ -465,25 +464,157 @@ $red-light: #fee2e2;
|
||||
// ── 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Senior Citizen Status Banner ──────────────────────────────────────────
|
||||
|
||||
.fdrc-sc-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
&--applied {
|
||||
background: #ede9fe;
|
||||
border: 1.5px solid #a78bfa;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
&__body {
|
||||
&--notice {
|
||||
background: #fef3c7;
|
||||
border: 1.5px solid #fcd34d;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__bonus {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rate Comparison Block (Normal → Senior) ───────────────────────────────
|
||||
|
||||
.fdrc-rate-compare {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
&--sc {
|
||||
// senior block styling via child classes
|
||||
}
|
||||
}
|
||||
|
||||
&__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: $gray-200;
|
||||
color: $gray-600;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
|
||||
&--sc {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
}
|
||||
}
|
||||
|
||||
&__val {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
|
||||
&--strike {
|
||||
color: $gray-600;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&--sc {
|
||||
color: #5b21b6;
|
||||
}
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: $gray-600;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: 22px;
|
||||
color: $gray-600;
|
||||
padding-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── SC Tag (maturity card header) ─────────────────────────────────────────
|
||||
|
||||
.fdrc-sc-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
padding: 3px 10px;
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// ── SC Maturity Row ───────────────────────────────────────────────────────
|
||||
|
||||
.fdrc-maturity-row--sc {
|
||||
.fdrc-maturity-row__label {
|
||||
color: #5b21b6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fdrc-maturity-row__value {
|
||||
color: #5b21b6;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,6 +861,151 @@ $red-light: #fee2e2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fdrc-rate-badge--senior {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
}
|
||||
|
||||
// ── Senior Citizen Rate Comparison View ───────────────────────────────────
|
||||
|
||||
.fdrc-senior-rate-view {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__normal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
&__senior {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: $gray-200;
|
||||
color: $gray-600;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
|
||||
&--sr {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
}
|
||||
}
|
||||
|
||||
&__strike {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: $gray-600;
|
||||
text-decoration: line-through;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__applied {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #5b21b6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: 18px;
|
||||
color: $gray-600;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__bonus {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 14px;
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
i {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fdrc-senior-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fdrc-calc-field--senior {
|
||||
flex: 0 0 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fdrc-senior-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #6366f1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrc-senior-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-weight: 600;
|
||||
color: #4338ca;
|
||||
font-size: 13px;
|
||||
|
||||
i {
|
||||
color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrc-senior-hint {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.fdrc-incentive-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
+33
-2
@@ -84,6 +84,8 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
@Input() recurringDepositsAccountTemplate: any;
|
||||
@Input() recurringDepositsAccountProductTemplate: any;
|
||||
@Input() submittedOnDate: Date | null = null;
|
||||
/** Active CBS base interest rate versions — when provided, overrides the product chart slabs */
|
||||
@Input() cbsBaseRates: any[] = [];
|
||||
|
||||
@Output() tenureSelected = new EventEmitter<{
|
||||
depositPeriod: number;
|
||||
@@ -92,9 +94,15 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
daysInYear?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
}>();
|
||||
|
||||
get interestRateChartData(): any[] {
|
||||
const activeBase = (this.cbsBaseRates || []).find((r) => r.fdType === 'DOUBLE' && r.isActive);
|
||||
if (activeBase?.slabs?.length) {
|
||||
return this.normalizeCbsSlabs(activeBase.slabs);
|
||||
}
|
||||
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
|
||||
if (!template) return [];
|
||||
return (
|
||||
@@ -107,13 +115,23 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeCbsSlabs(slabs: any[]): any[] {
|
||||
return (slabs || []).map((s) => ({
|
||||
...s,
|
||||
periodType: { id: s.periodTypeEnum, value: s.periodTypeLabel }
|
||||
}));
|
||||
}
|
||||
|
||||
chartSlabsDisplayedColumns: string[] = [
|
||||
'period',
|
||||
'amountRange',
|
||||
'interest',
|
||||
'seniorCitizenRate',
|
||||
'description',
|
||||
'actions'
|
||||
];
|
||||
|
||||
isSeniorCitizen = false;
|
||||
incentivesDisplayedColumns: string[] = [
|
||||
'entityType',
|
||||
'attributeName',
|
||||
@@ -208,7 +226,10 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
return;
|
||||
}
|
||||
|
||||
const rate = matchedSlab.annualInterestRate ?? 0;
|
||||
const rate =
|
||||
this.isSeniorCitizen && (matchedSlab.seniorCitizenRate ?? 0) > 0
|
||||
? (matchedSlab.seniorCitizenRate ?? 0)
|
||||
: (matchedSlab.annualInterestRate ?? 0);
|
||||
const result: SlabMatchResult = {
|
||||
slab: matchedSlab,
|
||||
slabIndex: matchedIndex,
|
||||
@@ -271,7 +292,9 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
depositAmount: this.principalAmount || undefined,
|
||||
maturityAmount: this.matchResult.maturityAmount,
|
||||
interestEarned: this.matchResult.interestEarned,
|
||||
daysInYear: this.matchResult.daysInYear
|
||||
daysInYear: this.matchResult.daysInYear,
|
||||
appliedRate: this.matchResult.appliedRate,
|
||||
isSeniorCitizen: this.isSeniorCitizen
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -298,6 +321,14 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
this.validationWarning = null;
|
||||
}
|
||||
|
||||
onSeniorCitizenChange() {
|
||||
if (this.tenureValue && this.tenureValue > 0) {
|
||||
this.calculateRate();
|
||||
} else {
|
||||
this.matchResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
isMatchedSlab(index: number): boolean {
|
||||
return this.matchResult?.slabIndex === index;
|
||||
}
|
||||
|
||||
+41
-6
@@ -609,8 +609,16 @@
|
||||
>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Applicable Interest Rate</span>
|
||||
<span class="preview-value">{{ rdApplicableSlab?.annualInterestRate | formatNumber }}%</span>
|
||||
<span class="preview-label">
|
||||
@if (rdIsSeniorCitizen && rdSeniorCitizenBonus > 0) {
|
||||
<i class="fas fa-user-shield" style="color: #5b21b6; margin-right: 4px"></i>Sr. Citizen Rate
|
||||
} @else {
|
||||
Applicable Interest Rate
|
||||
}
|
||||
</span>
|
||||
<span class="preview-value" [style.color]="rdIsSeniorCitizen && rdSeniorCitizenBonus > 0 ? '#5b21b6' : ''">
|
||||
{{ rdAppliedInterestRate | formatNumber }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Tenure</span>
|
||||
@@ -652,7 +660,26 @@
|
||||
<div class="preview-body">
|
||||
<!-- Selected Tenure + Rate Banner -->
|
||||
@if (rdTenureDays && matchedSlabIndex >= 0) {
|
||||
<div class="preview-tenure-banner">
|
||||
<!-- Senior Citizen Status Banner -->
|
||||
@if (rdIsSeniorCitizen) {
|
||||
@if (rdSeniorCitizenBonus > 0) {
|
||||
<div class="preview-sc-status preview-sc-status--applied">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span>Senior Citizen Rate Applied</span>
|
||||
<span class="preview-sc-status__bonus">+{{ rdSeniorCitizenBonus | formatNumber }}% bonus</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="preview-sc-status preview-sc-status--notice">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>Senior Citizen selected — no additional rate configured for this slab</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
class="preview-tenure-banner"
|
||||
[class.preview-tenure-banner--sc]="rdIsSeniorCitizen && rdSeniorCitizenBonus > 0"
|
||||
>
|
||||
<div class="preview-tenure-banner__left">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<div>
|
||||
@@ -662,9 +689,17 @@
|
||||
</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
|
||||
<span class="preview-tenure-banner__rate-label">
|
||||
@if (rdIsSeniorCitizen && rdSeniorCitizenBonus > 0) {
|
||||
<i class="fas fa-user-shield" style="color: #5b21b6; margin-right: 4px"></i>Sr. Citizen Rate
|
||||
} @else {
|
||||
Applicable Interest Rate
|
||||
}
|
||||
</span>
|
||||
<span
|
||||
class="preview-tenure-banner__rate"
|
||||
[style.color]="rdIsSeniorCitizen && rdSeniorCitizenBonus > 0 ? '#5b21b6' : ''"
|
||||
>{{ rdAppliedInterestRate | formatNumber }}%</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-tenure-banner__slab">
|
||||
|
||||
+48
@@ -283,6 +283,54 @@
|
||||
border-left: 3px solid #16a34a;
|
||||
}
|
||||
|
||||
// ── Senior Citizen Status Banner (preview) ────────────────────────────────────
|
||||
.preview-sc-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
&--applied {
|
||||
background: #ede9fe;
|
||||
border: 1.5px solid #a78bfa;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
&--notice {
|
||||
background: #fef3c7;
|
||||
border: 1.5px solid #fcd34d;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
&__bonus {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-tenure-banner--sc {
|
||||
border-color: #a78bfa;
|
||||
background: #f5f3ff;
|
||||
|
||||
.preview-tenure-banner__left > i {
|
||||
color: #5b21b6;
|
||||
}
|
||||
|
||||
.preview-tenure-banner__divider {
|
||||
background: #a78bfa;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Legend below table ────────────────────────────────────────────────────────
|
||||
.preview-slab-legend {
|
||||
display: flex;
|
||||
|
||||
+13
@@ -80,6 +80,19 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
|
||||
@Input() rdMaturityAmount: number | null = null;
|
||||
@Input() rdInterestEarned: number | null = null;
|
||||
@Input() rdDaysInYear: number | null = null;
|
||||
@Input() rdAppliedRate: number | null = null;
|
||||
@Input() rdIsSeniorCitizen = false;
|
||||
|
||||
get rdAppliedInterestRate(): number {
|
||||
if (this.rdAppliedRate != null) return this.rdAppliedRate;
|
||||
return this.rdApplicableSlab?.annualInterestRate ?? 0;
|
||||
}
|
||||
|
||||
get rdSeniorCitizenBonus(): number {
|
||||
if (!this.rdIsSeniorCitizen || this.rdAppliedRate == null) return 0;
|
||||
const normalRate = this.rdApplicableSlab?.annualInterestRate ?? 0;
|
||||
return this.rdAppliedRate - normalRate;
|
||||
}
|
||||
|
||||
/** Output the submit action */
|
||||
@Output() submitEvent = new EventEmitter();
|
||||
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
<!--
|
||||
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="page-layout">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>CBS Base Interest Rate</h2>
|
||||
<p class="page-subtitle">
|
||||
Manage master CBS interest rate slabs by deposit type. Saving a new version automatically
|
||||
<strong>deactivates</strong> the current active version and <strong>activates</strong> the new one. This chart
|
||||
is used as the reference for both Fixed Deposit and Recurring Deposit products.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-tab-group animationDuration="150ms">
|
||||
@for (fdType of fdTypes; track fdType) {
|
||||
<mat-tab [label]="fdType">
|
||||
<div class="tab-content">
|
||||
@if (!showPreview[fdType]) {
|
||||
<!-- ── Editable Slabs Table ── -->
|
||||
<div class="section-header">
|
||||
<h3>CBS Base Interest Rate Slabs</h3>
|
||||
<button type="button" mat-raised-button color="accent" (click)="addSlab(fdType)">
|
||||
<mat-icon>add</mat-icon> Add Slab
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (slabs(fdType).length === 0) {
|
||||
<p class="empty-text">No slabs defined. Click "Add Slab" to begin.</p>
|
||||
} @else {
|
||||
<form [formGroup]="forms[fdType]">
|
||||
<table mat-table [dataSource]="slabs(fdType).controls" class="slabs-table">
|
||||
<ng-container matColumnDef="srNo">
|
||||
<th mat-header-cell *matHeaderCellDef>#</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.get('srNo').value }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-form-field class="cell-field">
|
||||
<input matInput [formControl]="row.get('description')" placeholder="e.g. 7 Days to 14 Days" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="periodType">
|
||||
<th mat-header-cell *matHeaderCellDef>Period Type</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-form-field class="cell-field cell-field--sm">
|
||||
<mat-select [formControl]="row.get('periodTypeEnum')">
|
||||
@for (pt of periodTypes; track pt.id) {
|
||||
<mat-option [value]="pt.id">{{ pt.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="fromPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>From</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-form-field class="cell-field cell-field--xs">
|
||||
<input matInput type="number" min="0" [formControl]="row.get('fromPeriod')" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="toPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>To</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-form-field class="cell-field cell-field--xs">
|
||||
<input matInput type="number" min="0" [formControl]="row.get('toPeriod')" placeholder="∞" />
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="annualRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-form-field class="cell-field cell-field--xs">
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
[formControl]="row.get('annualInterestRate')"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="seniorRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Senior Citizen (%)</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-form-field class="cell-field cell-field--xs">
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
[formControl]="row.get('seniorCitizenRate')"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let row; let i = index">
|
||||
<button type="button" mat-icon-button color="warn" (click)="removeSlab(fdType, i)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="slabColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: slabColumns"></tr>
|
||||
</table>
|
||||
</form>
|
||||
}
|
||||
|
||||
<!-- ── Version Label & Remarks ── -->
|
||||
<form [formGroup]="forms[fdType]" class="version-form">
|
||||
<mat-form-field class="version-field">
|
||||
<mat-label>New Version Label</mat-label>
|
||||
<input matInput formControlName="versionLabel" placeholder="e.g. v2.0 May 2026" />
|
||||
<mat-error>Version label is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="version-field">
|
||||
<mat-label>Remarks (optional)</mat-label>
|
||||
<input matInput formControlName="remarks" />
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<div class="actions-row">
|
||||
<button
|
||||
type="button"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="forms[fdType].invalid || slabs(fdType).length === 0"
|
||||
(click)="openPreview(fdType)"
|
||||
>
|
||||
Preview & Save
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- ── Preview Panel ── -->
|
||||
<div class="preview-section">
|
||||
<div class="preview-heading">
|
||||
<h3>CBS Base Interest Rate — Save Preview ({{ fdType }})</h3>
|
||||
<p class="page-subtitle">
|
||||
Review the change before confirming. The current active version will be
|
||||
<strong class="text-red">deactivated</strong>, and the new version will be
|
||||
<strong class="text-green">activated</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="preview-grid">
|
||||
<!-- Current Active (will deactivate) -->
|
||||
<div class="preview-card preview-card--deactivate">
|
||||
<div class="preview-card-header">
|
||||
<span class="active-badge">Currently Active</span>
|
||||
<span class="deactivate-arrow">→ Will be DEACTIVATED</span>
|
||||
</div>
|
||||
@if (activeRates[fdType]) {
|
||||
<p class="version-meta">{{ activeRates[fdType].versionLabel }}</p>
|
||||
@if (activeRates[fdType].remarks) {
|
||||
<p class="remarks-text">{{ activeRates[fdType].remarks }}</p>
|
||||
}
|
||||
<table mat-table [dataSource]="activeRates[fdType].slabs" class="preview-table">
|
||||
<ng-container matColumnDef="srNo">
|
||||
<th mat-header-cell *matHeaderCellDef>#</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.srNo }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.description }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="periodType">
|
||||
<th mat-header-cell *matHeaderCellDef>Period</th>
|
||||
<td mat-cell *matCellDef="let s">{{ getPeriodLabel(s.periodTypeEnum) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fromPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>From</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.fromPeriod }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="toPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>To</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.toPeriod ?? '∞' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="annualRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.annualInterestRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="seniorRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Senior (%)</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.seniorCitizenRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="historySlabColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: historySlabColumns"></tr>
|
||||
</table>
|
||||
} @else {
|
||||
<p class="empty-text">No active version currently exists.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- New Version (will activate) -->
|
||||
<div class="preview-card preview-card--activate">
|
||||
<div class="preview-card-header">
|
||||
<span class="inactive-badge">New Version</span>
|
||||
<span class="activate-arrow">→ Will be ACTIVATED</span>
|
||||
</div>
|
||||
<p class="version-meta">{{ forms[fdType].value.versionLabel }}</p>
|
||||
@if (forms[fdType].value.remarks) {
|
||||
<p class="remarks-text">{{ forms[fdType].value.remarks }}</p>
|
||||
}
|
||||
<table mat-table [dataSource]="slabs(fdType).value" class="preview-table">
|
||||
<ng-container matColumnDef="srNo">
|
||||
<th mat-header-cell *matHeaderCellDef>#</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.srNo }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.description }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="periodType">
|
||||
<th mat-header-cell *matHeaderCellDef>Period</th>
|
||||
<td mat-cell *matCellDef="let s">{{ getPeriodLabel(s.periodTypeEnum) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fromPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>From</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.fromPeriod }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="toPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>To</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.toPeriod ?? '∞' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="annualRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.annualInterestRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="seniorRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Senior (%)</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.seniorCitizenRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="historySlabColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: historySlabColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-row">
|
||||
<button type="button" mat-stroked-button (click)="cancelPreview(fdType)">Cancel</button>
|
||||
<button
|
||||
type="button"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="isSaving[fdType]"
|
||||
(click)="confirmSave(fdType)"
|
||||
>
|
||||
{{ isSaving[fdType] ? 'Saving...' : 'Confirm & Activate' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (saveMessage[fdType]) {
|
||||
<p class="save-message">{{ saveMessage[fdType] }}</p>
|
||||
}
|
||||
|
||||
<!-- ── History ── -->
|
||||
<div class="history-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>CBS Base Interest Rate — Version History</h3>
|
||||
<p class="page-subtitle">All past versions for {{ fdType }} deposit type.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isLoading) {
|
||||
<p>Loading history...</p>
|
||||
} @else if (historyForType(fdType).length === 0) {
|
||||
<p class="empty-text">No history found.</p>
|
||||
} @else {
|
||||
<mat-accordion multi>
|
||||
@for (version of historyForType(fdType); track version.id) {
|
||||
<mat-expansion-panel [expanded]="version.isActive">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<span class="version-header">
|
||||
<strong>{{ version.versionLabel }}</strong>
|
||||
<span [class]="version.isActive ? 'active-badge' : 'inactive-badge'">
|
||||
{{ version.isActive ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</span>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<span class="meta-text">From {{ version.effectiveFromDate }}</span>
|
||||
<span class="meta-text">To {{ version.effectiveToDate || 'Current' }}</span>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
@if (version.remarks) {
|
||||
<p class="remarks-text">{{ version.remarks }}</p>
|
||||
}
|
||||
|
||||
<table mat-table [dataSource]="version.slabs" class="history-table">
|
||||
<ng-container matColumnDef="srNo">
|
||||
<th mat-header-cell *matHeaderCellDef>#</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.srNo }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.description }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="periodType">
|
||||
<th mat-header-cell *matHeaderCellDef>Period</th>
|
||||
<td mat-cell *matCellDef="let s">{{ getPeriodLabel(s.periodTypeEnum) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fromPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>From</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.fromPeriod }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="toPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>To</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.toPeriod ?? '∞' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="annualRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.annualInterestRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="seniorRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Senior Citizen (%)</th>
|
||||
<td mat-cell *matCellDef="let s">{{ s.seniorCitizenRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="historySlabColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: historySlabColumns"></tr>
|
||||
</table>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
}
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.page-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 1rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0.25rem 0 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
padding: 1.25rem 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Section Header ── */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Slabs Table ── */
|
||||
.slabs-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cell-field {
|
||||
width: 100%;
|
||||
min-width: 140px;
|
||||
|
||||
&--sm {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
&--xs {
|
||||
min-width: 80px;
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Version Form ── */
|
||||
.version-form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.version-field {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* ── Actions ── */
|
||||
.actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.save-message {
|
||||
color: #16a34a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Preview Section ── */
|
||||
.preview-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.preview-heading h3 {
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
@media (width <= 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&--deactivate {
|
||||
background: #fff7f7;
|
||||
border-color: #fca5a5;
|
||||
}
|
||||
|
||||
&--activate {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.deactivate-arrow {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.activate-arrow {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.preview-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── History ── */
|
||||
.history-section {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.version-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
margin-left: 0.75rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remarks-text {
|
||||
margin: 0 0 0.75rem;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.version-meta {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Badges ── */
|
||||
.active-badge,
|
||||
.inactive-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active-badge {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.inactive-badge {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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, OnInit, inject } from '@angular/core';
|
||||
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-base-fd-interest-rate-chart',
|
||||
templateUrl: './base-fd-interest-rate-chart.component.html',
|
||||
styleUrls: ['./base-fd-interest-rate-chart.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatTabsModule,
|
||||
MatIconButton,
|
||||
MatIconModule,
|
||||
MatExpansionModule,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
DecimalPipe
|
||||
]
|
||||
})
|
||||
export class BaseFdInterestRateChartComponent implements OnInit {
|
||||
private fb = inject(FormBuilder);
|
||||
private productsService = inject(ProductsService);
|
||||
|
||||
readonly fdTypes = [
|
||||
'SINGLE',
|
||||
'DOUBLE',
|
||||
'SPECIAL'
|
||||
];
|
||||
readonly periodTypes = [
|
||||
{ id: 0, label: 'Days' },
|
||||
{ id: 1, label: 'Weeks' },
|
||||
{ id: 2, label: 'Months' },
|
||||
{ id: 3, label: 'Years' }
|
||||
];
|
||||
readonly slabColumns = [
|
||||
'srNo',
|
||||
'description',
|
||||
'periodType',
|
||||
'fromPeriod',
|
||||
'toPeriod',
|
||||
'annualRate',
|
||||
'seniorRate',
|
||||
'actions'
|
||||
];
|
||||
readonly historySlabColumns = [
|
||||
'srNo',
|
||||
'description',
|
||||
'periodType',
|
||||
'fromPeriod',
|
||||
'toPeriod',
|
||||
'annualRate',
|
||||
'seniorRate'
|
||||
];
|
||||
|
||||
forms: Record<string, FormGroup> = {};
|
||||
activeRates: Record<string, any> = {};
|
||||
historyRates: any[] = [];
|
||||
|
||||
isLoading = false;
|
||||
isSaving: Record<string, boolean> = {};
|
||||
saveMessage: Record<string, string> = {};
|
||||
showPreview: Record<string, boolean> = {};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fdTypes.forEach((type) => {
|
||||
this.forms[type] = this.buildForm();
|
||||
this.isSaving[type] = false;
|
||||
this.saveMessage[type] = '';
|
||||
this.showPreview[type] = false;
|
||||
});
|
||||
this.loadAll();
|
||||
}
|
||||
|
||||
slabs(fdType: string): FormArray {
|
||||
return this.forms[fdType].get('slabs') as FormArray;
|
||||
}
|
||||
|
||||
addSlab(fdType: string): void {
|
||||
const arr = this.slabs(fdType);
|
||||
arr.push(this.buildSlabRow({ srNo: arr.length + 1 }));
|
||||
}
|
||||
|
||||
removeSlab(fdType: string, index: number): void {
|
||||
this.slabs(fdType).removeAt(index);
|
||||
this.slabs(fdType).controls.forEach((c, i) => c.patchValue({ srNo: i + 1 }));
|
||||
}
|
||||
|
||||
openPreview(fdType: string): void {
|
||||
this.showPreview[fdType] = true;
|
||||
this.saveMessage[fdType] = '';
|
||||
}
|
||||
|
||||
cancelPreview(fdType: string): void {
|
||||
this.showPreview[fdType] = false;
|
||||
}
|
||||
|
||||
confirmSave(fdType: string): void {
|
||||
this.isSaving[fdType] = true;
|
||||
const { versionLabel, remarks, slabs } = this.forms[fdType].value;
|
||||
this.productsService.saveFdBaseInterestRates({ fdType, versionLabel, remarks, slabs }).subscribe({
|
||||
next: () => {
|
||||
this.saveMessage[fdType] = 'New version activated. Previous version deactivated.';
|
||||
this.showPreview[fdType] = false;
|
||||
this.isSaving[fdType] = false;
|
||||
this.loadAll();
|
||||
},
|
||||
error: () => {
|
||||
this.saveMessage[fdType] = 'Unable to save. Please try again.';
|
||||
this.isSaving[fdType] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPeriodLabel(enumVal: number): string {
|
||||
return this.periodTypes.find((p) => p.id === enumVal)?.label ?? String(enumVal);
|
||||
}
|
||||
|
||||
historyForType(fdType: string): any[] {
|
||||
return this.historyRates
|
||||
.filter((v) => v.fdType === fdType)
|
||||
.sort((a, b) => {
|
||||
if (a.isActive && !b.isActive) return -1;
|
||||
if (!a.isActive && b.isActive) return 1;
|
||||
const dateA = a.effectiveFromDate ? new Date(a.effectiveFromDate).getTime() : 0;
|
||||
const dateB = b.effectiveFromDate ? new Date(b.effectiveFromDate).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
|
||||
private loadAll(): void {
|
||||
this.isLoading = true;
|
||||
this.fdTypes.forEach((type) => {
|
||||
this.productsService.getFdBaseInterestRates(type).subscribe({
|
||||
next: (rates: any[]) => {
|
||||
const active = (rates || []).find((r: any) => r.isActive);
|
||||
this.activeRates[type] = active ?? null;
|
||||
const arr = this.slabs(type);
|
||||
arr.clear();
|
||||
(active?.slabs ?? []).forEach((s: any) => arr.push(this.buildSlabRow(s)));
|
||||
this.forms[type].patchValue({ versionLabel: '', remarks: '' });
|
||||
},
|
||||
error: () => {
|
||||
this.activeRates[type] = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
this.productsService.getFdBaseInterestRateHistory().subscribe({
|
||||
next: (h: any[]) => {
|
||||
this.historyRates = h || [];
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.historyRates = [];
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
versionLabel: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
remarks: [''],
|
||||
slabs: this.fb.array([])
|
||||
});
|
||||
}
|
||||
|
||||
private buildSlabRow(d?: any): FormGroup {
|
||||
return this.fb.group({
|
||||
srNo: [d?.srNo ?? 1],
|
||||
description: [
|
||||
d?.description ?? '',
|
||||
Validators.required
|
||||
],
|
||||
periodTypeEnum: [
|
||||
d?.periodTypeEnum ?? 2,
|
||||
Validators.required
|
||||
],
|
||||
fromPeriod: [
|
||||
d?.fromPeriod ?? 0,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
]
|
||||
],
|
||||
toPeriod: [d?.toPeriod ?? null],
|
||||
annualInterestRate: [
|
||||
d?.annualInterestRate ?? 0,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
]
|
||||
],
|
||||
seniorCitizenRate: [
|
||||
d?.seniorCitizenRate ?? 0,
|
||||
Validators.min(0)
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ import { EditProductMixComponent } from './products-mix/edit-product-mix/edit-pr
|
||||
import { ChargesTemplateAndResolver } from './charges/charges-template-and-resolver';
|
||||
import { EditFixedDepositProductComponent } from './fixed-deposit-products/edit-fixed-deposit-product/edit-fixed-deposit-product.component';
|
||||
import { FixedDepositProductAndTemplateResolver } from './fixed-deposit-products/edit-fixed-deposit-product/fixed-deposit-product-and-template.resolver';
|
||||
import { BaseFdInterestRateChartComponent } from './fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component';
|
||||
import { CollateralResolver } from './collaterals/collateral.resolver';
|
||||
import { CollateralsResolver } from './collaterals/collaterals.resolver';
|
||||
import { CollateralTemplateResolver } from './collaterals/collaterals-template.resolver';
|
||||
@@ -727,6 +728,11 @@ const routes: Routes = [
|
||||
path: 'fixed-deposit-products',
|
||||
data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' },
|
||||
children: [
|
||||
{
|
||||
path: 'base-interest-rates',
|
||||
component: BaseFdInterestRateChartComponent,
|
||||
data: { title: 'CBS Base Interest Rate', breadcrumb: 'CBS Base Interest Rate' }
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
component: CreateFixedDepositProductComponent,
|
||||
|
||||
@@ -87,6 +87,25 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
class="pd-card"
|
||||
[routerLink]="['fixed-deposit-products', 'base-interest-rates']"
|
||||
*mifosxHasPermission="'READ_FIXEDDEPOSITPRODUCT'"
|
||||
>
|
||||
<div class="pd-card__icon pd-card__icon--amber">
|
||||
<i class="fas fa-percentage"></i>
|
||||
</div>
|
||||
<div class="pd-card__body">
|
||||
<h3 class="pd-card__title">CBS Base Interest Rate</h3>
|
||||
<p class="pd-card__desc">
|
||||
Manage and activate CBS base interest rate slabs used for Fixed and Recurring Deposit products
|
||||
</p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-right pd-card__arrow"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div #recurringDepositProducts>
|
||||
<a
|
||||
class="pd-card"
|
||||
|
||||
@@ -84,7 +84,6 @@ import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-t
|
||||
import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components';
|
||||
import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component';
|
||||
import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component';
|
||||
import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component';
|
||||
import { RecurringDepositProductAccountingStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component';
|
||||
import { RecurringDepositProductChargesStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component';
|
||||
import { RecurringDepositProductCurrencyStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component';
|
||||
@@ -214,7 +213,6 @@ import { AdvancedAccountingMappingRuleComponent } from './loan-products/loan-pro
|
||||
DepositProductIncentiveFormDialogComponent,
|
||||
ViewRecurringDepositProductComponent,
|
||||
CreateRecurringDepositProductComponent,
|
||||
BaseRecurringDepositInterestRateChartComponent,
|
||||
RecurringDepositProductAccountingStepComponent,
|
||||
RecurringDepositProductChargesStepComponent,
|
||||
RecurringDepositProductCurrencyStepComponent,
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
<!--
|
||||
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="page-layout">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>CBS Base Recurring Deposit Interest Chart</h2>
|
||||
<p class="page-subtitle">This chart is the master template used when creating a recurring deposit product.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (recurringDepositProductsTemplate?.chartTemplate) {
|
||||
<mifosx-recurring-deposit-product-interest-rate-chart-step
|
||||
#stepComponent
|
||||
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
|
||||
[rdInterestRates]="rdInterestRates"
|
||||
>
|
||||
</mifosx-recurring-deposit-product-interest-rate-chart-step>
|
||||
|
||||
<div class="actions-row">
|
||||
<button
|
||||
type="button"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="isSaving || !stepComponent"
|
||||
(click)="save(stepComponent)"
|
||||
>
|
||||
{{ isSaving ? 'Saving...' : 'Save CBS Base Chart' }}
|
||||
</button>
|
||||
@if (saveMessage) {
|
||||
<span class="save-message">{{ saveMessage }}</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p>Loading CBS base chart...</p>
|
||||
}
|
||||
|
||||
<div class="history-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>Chart History</h3>
|
||||
<p class="section-subtitle">Previous CBS base chart versions are listed here for comparison and audit.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isHistoryLoading) {
|
||||
<p>Loading history...</p>
|
||||
} @else if (rdInterestRateHistory.length === 0) {
|
||||
<p>No history found for the CBS base chart.</p>
|
||||
} @else {
|
||||
<mat-accordion multi>
|
||||
@for (version of rdInterestRateHistory; track version.id) {
|
||||
<mat-expansion-panel [expanded]="version.isActive">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<span class="version-header">
|
||||
<strong>{{ version.versionLabel }}</strong>
|
||||
<span [class]="version.isActive ? 'active-badge' : 'inactive-badge'">
|
||||
{{ version.isActive ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</span>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<span class="meta-text">From {{ version.effectiveFromDate }}</span>
|
||||
<span class="meta-text">To {{ version.effectiveToDate || 'Current' }}</span>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
@if (version.remarks) {
|
||||
<p class="remarks-text">{{ version.remarks }}</p>
|
||||
}
|
||||
|
||||
<table class="history-table" mat-table [dataSource]="version.slabs">
|
||||
<ng-container matColumnDef="srNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Sr.</th>
|
||||
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="fromPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>From</th>
|
||||
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="toPeriod">
|
||||
<th mat-header-cell *matHeaderCellDef>To</th>
|
||||
<td mat-cell *matCellDef="let slab">
|
||||
{{ slab.toPeriod !== null && slab.toPeriod !== undefined ? slab.toPeriod : 'N/A' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="periodTypeLabel">
|
||||
<th mat-header-cell *matHeaderCellDef>Period Type</th>
|
||||
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="annualInterestRate">
|
||||
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
|
||||
<td mat-cell *matCellDef="let slab">{{ slab.annualInterestRate | number: '1.2-2' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="historySlabColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: historySlabColumns"></tr>
|
||||
</table>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.page-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0.25rem 0 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.save-message {
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
margin: 0.25rem 0 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.version-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
margin-left: 0.75rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.active-badge,
|
||||
.inactive-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active-badge {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.inactive-badge {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.remarks-text {
|
||||
margin: 0 0 0.75rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* 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, OnInit, inject } from '@angular/core';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-base-recurring-deposit-interest-rate-chart',
|
||||
templateUrl: './base-recurring-deposit-interest-rate-chart.component.html',
|
||||
styleUrls: ['./base-recurring-deposit-interest-rate-chart.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatButton,
|
||||
MatExpansionModule,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
DecimalPipe,
|
||||
RecurringDepositProductInterestRateChartStepComponent
|
||||
]
|
||||
})
|
||||
export class BaseRecurringDepositInterestRateChartComponent implements OnInit {
|
||||
private productsService = inject(ProductsService);
|
||||
|
||||
recurringDepositProductsTemplate: any;
|
||||
rdInterestRates: any[] = [];
|
||||
rdInterestRateHistory: any[] = [];
|
||||
readonly historySlabColumns = [
|
||||
'srNo',
|
||||
'description',
|
||||
'fromPeriod',
|
||||
'toPeriod',
|
||||
'periodTypeLabel',
|
||||
'annualInterestRate'
|
||||
];
|
||||
isSaving = false;
|
||||
isHistoryLoading = false;
|
||||
saveMessage = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.productsService.getRecurringDepositProductsTemplate().subscribe({
|
||||
next: (template: any) => (this.recurringDepositProductsTemplate = template),
|
||||
error: () => (this.recurringDepositProductsTemplate = null)
|
||||
});
|
||||
|
||||
this.loadBaseRates();
|
||||
this.loadBaseRateHistory();
|
||||
}
|
||||
|
||||
get chartStep(): RecurringDepositProductInterestRateChartStepComponent | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
save(stepComponent: RecurringDepositProductInterestRateChartStepComponent): void {
|
||||
this.isSaving = true;
|
||||
this.saveMessage = '';
|
||||
const payload = stepComponent.modifiedRdInterestRatePayload;
|
||||
this.productsService.saveRdBaseInterestRates(payload).subscribe({
|
||||
next: () => {
|
||||
this.saveMessage = 'CBS base chart saved successfully.';
|
||||
this.loadBaseRates();
|
||||
this.loadBaseRateHistory();
|
||||
this.isSaving = false;
|
||||
},
|
||||
error: () => {
|
||||
this.saveMessage = 'Unable to save CBS base chart.';
|
||||
this.isSaving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadBaseRates(): void {
|
||||
this.productsService.getRdBaseInterestRates().subscribe({
|
||||
next: (rates: any[]) => (this.rdInterestRates = rates || []),
|
||||
error: () => (this.rdInterestRates = [])
|
||||
});
|
||||
}
|
||||
|
||||
private loadBaseRateHistory(): void {
|
||||
this.isHistoryLoading = true;
|
||||
this.productsService.getRdBaseInterestRateHistory().subscribe({
|
||||
next: (history: any[]) => {
|
||||
this.rdInterestRateHistory = history || [];
|
||||
this.isHistoryLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.rdInterestRateHistory = [];
|
||||
this.isHistoryLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
@@ -226,6 +226,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="soa-add-btn"
|
||||
[disabled]="totalJointSharePercentage >= 100"
|
||||
(click)="openAddJointHolderDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> Add Holder
|
||||
|
||||
+2
-1
@@ -334,11 +334,12 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
...(this.primaryClientId ? [this.primaryClientId] : []),
|
||||
...this.jointHolders.map((h: any) => h.clientId).filter(Boolean)
|
||||
];
|
||||
const maxSharePercentage = Math.max(0, 100 - this.totalJointSharePercentage);
|
||||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||||
width: '500px',
|
||||
panelClass: 'arc-dialog-panel',
|
||||
disableClose: true,
|
||||
data: { excludedClientIds }
|
||||
data: { excludedClientIds, maxSharePercentage }
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260520',
|
||||
'hash': 'bf3e26a0'
|
||||
'version': '260521',
|
||||
'hash': '846b2484'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user