From 7c868ae07314e345cbe9b56000d56a0b40454660 Mon Sep 17 00:00:00 2001 From: Sonali Likhitkar Date: Mon, 5 Oct 2026 16:20:36 +0530 Subject: [PATCH] feat(loan-products): add loan interest rate register UI - Add Loan Interest Rate Register screen with CIBIL adjustment and edit-register views, backed by rate-register service and model - Wire the new screen into products routing and the products menu - Update loan product details and advance limit/tenure steps Co-Authored-By: Claude Sonnet 5.5 --- .../cibil-adjustment.component.html | 166 +++++++ .../cibil-adjustment.component.scss | 344 +++++++++++++ .../cibil-adjustment.component.ts | 162 +++++++ .../edit-register.component.html | 114 +++++ .../edit-register.component.scss | 323 +++++++++++++ .../edit-register/edit-register.component.ts | 196 ++++++++ ...loan-interest-rate-register.component.html | 83 ++++ ...loan-interest-rate-register.component.scss | 212 ++++++++ .../loan-interest-rate-register.component.ts | 78 +++ .../models/rate-register.model.ts | 52 ++ .../rate-register.service.ts | 457 ++++++++++++++++++ ...uct-advance-limit-tenure-step.component.ts | 2 +- .../loan-product-details-step.component.html | 6 +- .../loan-product-details-step.component.ts | 32 +- src/app/products/products-routing.module.ts | 23 + src/app/products/products.component.html | 13 + 16 files changed, 2252 insertions(+), 11 deletions(-) create mode 100644 src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.html create mode 100644 src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.scss create mode 100644 src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.ts create mode 100644 src/app/products/loan-interest-rate-register/edit-register/edit-register.component.html create mode 100644 src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss create mode 100644 src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts create mode 100644 src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.html create mode 100644 src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss create mode 100644 src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts create mode 100644 src/app/products/loan-interest-rate-register/models/rate-register.model.ts create mode 100644 src/app/products/loan-interest-rate-register/rate-register.service.ts diff --git a/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.html b/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.html new file mode 100644 index 000000000..92b80c9de --- /dev/null +++ b/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.html @@ -0,0 +1,166 @@ + + +
+
+
CS
+
+

CIBIL / credit score adjustment

+

Applied on top of the chart rate at loan sanction · reused across all loan products linked to this register

+
+ +
+ +
+
+
+
+
Credit score slabs
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
#Score fromScore toRisk tierTypeAdjustment
{{ i + 1 }}{{ slab.from }}{{ slab.to }} + {{ slab.tier }} + {{ slab.type }} + {{ formatPct(slab.adjustmentPct) }} + ⋯
+
+ +
+
+
Fallback behaviour
+
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
Live preview
+
pick a scenario
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
RATE DERIVATION
+
+ Chart cell — {{ productLabel(previewProductSrNo) }} + {{ + chartCell() !== null ? (chartCell() | number: '1.2-2') + '%' : 'N.A.' + }} +
+
+ CIBIL {{ previewCibil }} ({{ cibilSlab()?.tier ?? '—' }}) + + {{ formatPct(cibilSlab()?.adjustmentPct ?? 0) }} + +
+
+ Product floor / ceiling + no clamp +
+
+
+ Final rate + {{ finalRate() }} +
+
+
+
+ +
+
+
What changes if…
+
+
+ CIBIL = 700 + {{ rateFor(700) }} +
+
+ CIBIL = 870 + {{ rateFor(870) }} +
+
+ No CIBIL fetched + {{ rateForFallback() }} +
+
+
+
+
+
+ + +
diff --git a/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.scss b/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.scss new file mode 100644 index 000000000..243151aa0 --- /dev/null +++ b/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.scss @@ -0,0 +1,344 @@ +/** + * 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/. + */ + +.ca { + background: #f8fafc; + min-height: calc(100vh - 160px); + padding: 20px 32px; + display: flex; + flex-direction: column; + gap: 16px; + + &__header { + display: flex; + align-items: center; + gap: 14px; + padding: 20px 24px; + background: white; + border: 1px solid #e2e8f0; + border-radius: 10px; + } + + &__header-icon { + width: 40px; + height: 40px; + border-radius: 9px; + background: #dbeafe; + color: #1d4ed8; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + font-weight: 700; + } + + &__header-text { + flex: 1; + + h2 { + margin: 0; + font-size: 20px; + font-weight: 600; + } + + p { + margin: 3px 0 0; + font-size: 12px; + color: #64748b; + } + } + + &__body { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 16px; + } + + &__left, + &__right { + display: flex; + flex-direction: column; + gap: 14px; + } + + &__card { + background: white; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; + } + + &__card-head { + padding: 12px 16px; + border-bottom: 1px solid #f1f5f9; + display: flex; + align-items: center; + gap: 10px; + } + + &__card-title { + font-size: 14px; + font-weight: 600; + } + + &__card-hint { + margin-left: auto; + font-size: 11px; + color: #64748b; + } + + &__card-body { + padding: 14px 18px; + display: flex; + flex-direction: column; + gap: 12px; + } + + &__dash-btn { + margin-left: auto; + height: 28px; + font-size: 11px; + font-weight: 600; + color: #1d4ed8; + border: 1px dashed #1d4ed8; + + i { + margin-right: 3px; + } + } + + &__table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + thead tr { + background: #f8fafc; + color: #64748b; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.3px; + + th { + text-align: left; + padding: 9px 14px; + font-weight: 600; + } + } + + tbody tr { + border-top: 1px solid #f1f5f9; + + td { + padding: 11px 14px; + } + } + } + + &__row-alt { + background: #fafbfc; + } + + &__num { + text-align: right; + } + + &__dots { + text-align: center; + color: #94a3b8; + cursor: pointer; + } + + &__fallback-grid, + &__preview-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + } + + &__field { + display: flex; + flex-direction: column; + + label { + font-size: 11px; + color: #64748b; + font-weight: 600; + letter-spacing: 0.3px; + margin-bottom: 4px; + } + + input, + select { + height: 36px; + border: 1px solid #cbd5e1; + background: white; + border-radius: 6px; + padding: 0 12px; + font-size: 13px; + color: #0f172a; + font-family: inherit; + + &:focus { + outline: 2px solid #1d4ed8; + outline-offset: -1px; + } + } + } + + &__derivation { + padding: 12px 14px; + background: #f8fafc; + border-radius: 8px; + } + + &__derivation-label { + font-size: 11px; + color: #64748b; + font-weight: 600; + letter-spacing: 0.3px; + margin-bottom: 8px; + } + + &__derivation-row { + display: flex; + justify-content: space-between; + font-size: 12px; + padding: 3px 0; + + &--muted { + color: #64748b; + } + } + + &__derivation-val { + font-weight: 600; + + &--pos { + color: #059669; + } + + &--neg { + color: #dc2626; + } + } + + &__divider { + height: 1px; + background: #e2e8f0; + margin: 8px 0; + } + + &__final { + display: flex; + justify-content: space-between; + align-items: center; + + span:first-child { + font-size: 12px; + font-weight: 600; + } + } + + &__final-val { + font-size: 20px; + font-weight: 700; + color: #1d4ed8; + } + + &__scenarios { + display: flex; + flex-direction: column; + gap: 8px; + font-size: 12px; + color: #334155; + } + + &__scenario { + padding: 8px 10px; + background: #f8fafc; + border-radius: 6px; + display: flex; + justify-content: space-between; + + span:last-child { + font-weight: 600; + } + } + + &__scenario-val--pos { + color: #059669; + } + + &__scenario-val--warn { + color: #d97706; + } + + &__footer { + background: white; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 10px 16px; + display: flex; + align-items: center; + justify-content: space-between; + } + + &__footer-note { + font-size: 11px; + color: #64748b; + } +} + +.tier { + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + + &--poor { + background: #fee2e2; + color: #991b1b; + } + + &--fair { + background: #fef3c7; + color: #92400e; + } + + &--good { + background: #dbeafe; + color: #1e40af; + } + + &--vgood { + background: #dcfce7; + color: #166534; + } + + &--excellent { + background: #d1fae5; + color: #065f46; + } +} + +.adj { + font-weight: 600; + + &--negative { + color: #059669; + } + + &--positive { + color: #d97706; + } + + &--high { + color: #dc2626; + } +} diff --git a/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.ts b/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.ts new file mode 100644 index 000000000..4a183cabf --- /dev/null +++ b/src/app/products/loan-interest-rate-register/cibil-adjustment/cibil-adjustment.component.ts @@ -0,0 +1,162 @@ +/** + * 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 { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { RateRegisterService } from '../rate-register.service'; +import { CibilSlab, RateRegister } from '../models/rate-register.model'; + +/** + * CIBIL/credit-score adjustment editor for a Rate Register — slab table, fallback behaviour + * and a live preview that resolves {product, loan amount, CIBIL} down to the final rate. + */ +@Component({ + selector: 'mifosx-cibil-adjustment', + templateUrl: './cibil-adjustment.component.html', + styleUrls: ['./cibil-adjustment.component.scss'], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule + ] +}) +export class CibilAdjustmentComponent implements OnInit { + private readonly service = inject(RateRegisterService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + + register!: RateRegister; + + // Live preview inputs. + previewProductSrNo = 7; // House Purchase / Construction + previewAmount = 2500000; + previewCibil = 820; + previewTenureMonths = 120; + + ngOnInit(): void { + const idParam = this.route.snapshot.queryParamMap.get('id'); + const id = idParam ? Number(idParam) : null; + this.service.list().subscribe((all) => { + this.register = (id ? all.find((r) => r.id === id) : undefined) ?? this.service.getActiveOrFirstDraft(); + }); + } + + tierClass(tier: CibilSlab['tier']): string { + const map: Record = { + Poor: 'tier tier--poor', + Fair: 'tier tier--fair', + Good: 'tier tier--good', + 'V. Good': 'tier tier--vgood', + Excellent: 'tier tier--excellent' + }; + return map[tier] ?? 'tier'; + } + + adjustmentClass(pct: number): string { + if (pct < 0) { + return 'adj adj--negative'; + } + if (pct > 0) { + return pct >= 1 ? 'adj adj--high' : 'adj adj--positive'; + } + return 'adj'; + } + + formatPct(pct: number): string { + if (pct === 0) { + return '0.00%'; + } + const sign = pct > 0 ? '+' : '−'; + return `${sign}${Math.abs(pct).toFixed(2)}%`; + } + + /** Looks up the chart cell for the currently previewed scenario. */ + private chartRateFor(productSrNo: number, amount: number): number | null { + const row = this.register.rows.find((r) => r.srNo === productSrNo); + if (!row) { + return null; + } + for (let i = 0; i < this.register.slabs.length; i++) { + const slab = this.register.slabs[i]; + const upto = slab.amountUpto; + if (upto === null || amount <= upto) { + const cell = row.rates[i]; + if (cell === null) { + return null; + } + if (typeof cell === 'string') { + // Formula cell — in a real derive we'd resolve against the pledged deposit's rate. + return null; + } + return cell; + } + } + return null; + } + + private cibilAdjustmentFor(score: number): CibilSlab | null { + return this.register.cibilSlabs.find((s) => score >= s.from && score <= s.to) ?? null; + } + + chartCell(): number | null { + return this.chartRateFor(this.previewProductSrNo, this.previewAmount); + } + + cibilSlab(): CibilSlab | null { + return this.cibilAdjustmentFor(this.previewCibil); + } + + finalRate(): string { + const base = this.chartCell(); + const slab = this.cibilSlab(); + if (base === null) { + return 'N.A.'; + } + const adjustment = slab?.adjustmentPct ?? 0; + return `${(base + adjustment).toFixed(2)}% p.a.`; + } + + productLabel(srNo: number): string { + return this.register.rows.find((r) => r.srNo === srNo)?.particulars ?? '—'; + } + + rateFor(cibil: number): string { + const base = this.chartRateFor(this.previewProductSrNo, this.previewAmount); + if (base === null) { + return 'N.A.'; + } + const slab = this.cibilAdjustmentFor(cibil); + return `${(base + (slab?.adjustmentPct ?? 0)).toFixed(2)}%`; + } + + rateForFallback(): string { + const base = this.chartRateFor(this.previewProductSrNo, this.previewAmount); + if (base === null) { + return 'N.A.'; + } + const fallback = this.register.cibilSlabs[this.register.fallbackNoCibilSlab - 1]; + return `${(base + (fallback?.adjustmentPct ?? 0)).toFixed(2)}%`; + } + + backToRegister(): void { + this.router.navigate(['/products/loan-interest-rate-register/edit'], { queryParams: { id: this.register.id } }); + } + + save(): void { + this.backToRegister(); + } +} diff --git a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.html b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.html new file mode 100644 index 000000000..02d72204d --- /dev/null +++ b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.html @@ -0,0 +1,114 @@ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+
Rate chart grid
+
+ {{ rows.length }} products × {{ slabs.length }} amount slabs · click any cell to edit +
+
+ + +
+
+
+ + + + + + + + + + + + + +
Particulars{{ slab.label }}
+ {{ row.srNo }}{{ row.particulars }} + + + {{ display(cell) }} +
+
+
+ +
+
CS
+
+
CIBIL / credit score adjustment
+
+ {{ register.cibilSlabs.length }} slabs · +1.00% (poor) to −0.20% (excellent) · applied on top of chart rate +
+
+ Edit → +
+ + +
diff --git a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss new file mode 100644 index 000000000..e784c658d --- /dev/null +++ b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.scss @@ -0,0 +1,323 @@ +/** + * 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/. + */ + +.er { + background: #f8fafc; + min-height: calc(100vh - 160px); + padding: 20px 24px; + display: flex; + flex-direction: column; + gap: 12px; + + &__meta-bar { + background: white; + border: 1px solid #e2e8f0; + border-radius: 9px; + padding: 12px 14px; + display: flex; + gap: 10px; + align-items: flex-end; + } + + &__field { + display: flex; + flex-direction: column; + flex: 1.5; + + label { + font-size: 10px; + color: #64748b; + font-weight: 600; + letter-spacing: 0.3px; + margin-bottom: 3px; + } + + input, + select { + height: 32px; + border: 1px solid #cbd5e1; + background: white; + border-radius: 6px; + padding: 0 10px; + font-size: 13px; + font-family: inherit; + color: #0f172a; + + &:focus { + outline: 2px solid #1d4ed8; + outline-offset: -1px; + } + } + + &--date { + flex: 0.9; + } + + &--curr { + flex: 0.5; + } + } + + &__meta-btn { + height: 32px; + font-size: 11px; + + i { + margin-right: 4px; + } + } + + &__grid-card { + background: white; + border: 1px solid #e2e8f0; + border-radius: 9px; + overflow: hidden; + display: flex; + flex-direction: column; + flex: 1; + min-height: 500px; + } + + &__grid-head { + padding: 8px 14px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + display: flex; + align-items: center; + gap: 10px; + } + + &__grid-title { + font-size: 12px; + font-weight: 600; + } + + &__grid-sub { + font-size: 10px; + color: #64748b; + } + + &__grid-actions { + margin-left: auto; + display: flex; + gap: 6px; + } + + &__dash-btn { + height: 26px; + font-size: 10px; + font-weight: 600; + color: #1d4ed8; + border: 1px dashed #1d4ed8; + + i { + margin-right: 3px; + } + } + + &__grid-scroll { + overflow: auto; + flex: 1; + } + + &__grid { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 11px; + + thead tr { + background: #f8fafc; + color: #475569; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.3px; + + th { + padding: 8px 7px; + font-weight: 600; + position: sticky; + top: 0; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + z-index: 2; + white-space: nowrap; + } + } + + tbody tr { + border-top: 1px solid #f1f5f9; + } + + td { + padding: 7px; + text-align: right; + cursor: text; + + &:first-child { + text-align: left; + } + } + } + + &__col-particulars { + position: sticky; + left: 0; + background: white; + border-right: 1px solid #e2e8f0; + font-weight: 500; + min-width: 240px; + text-align: left !important; + padding-left: 12px !important; + } + + &__row-alt &__col-particulars { + background: #fafbfc; + } + + &__row-alt { + background: #fafbfc; + } + + &__row--formula { + .er__col-particulars { + background: #eff6ff; + color: #1d4ed8; + font-weight: 600; + } + } + + &__srno { + color: #94a3b8; + margin-right: 6px; + } + + &__cell { + cursor: cell; + transition: background-color 0.1s; + + &:hover { + background: #f1f5f9; + outline: 1px solid #cbd5e1; + outline-offset: -1px; + } + + &--na { + color: #cbd5e1; + } + + &--formula { + background: #eff6ff; + color: #1d4ed8; + font-weight: 600; + } + + &--editing { + padding: 0 !important; + background: #fef9c3 !important; + outline: 2px solid #1d4ed8 !important; + outline-offset: -2px; + } + } + + &__cell-input { + width: 100%; + height: 100%; + min-height: 28px; + border: none; + outline: none; + background: transparent; + padding: 7px; + font-size: 11px; + text-align: right; + font-family: inherit; + color: #0f172a; + box-sizing: border-box; + } + + &__cibil-strip { + background: white; + border: 1px solid #e2e8f0; + border-radius: 9px; + padding: 9px 14px; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + transition: background-color 0.1s; + + &:hover { + background: #f8fafc; + } + } + + &__cibil-badge { + width: 24px; + height: 24px; + border-radius: 12px; + background: #dbeafe; + color: #1d4ed8; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + } + + &__cibil-text { + flex: 1; + } + + &__cibil-title { + font-size: 12px; + font-weight: 600; + } + + &__cibil-sub { + font-size: 10px; + color: #64748b; + margin-top: 1px; + } + + &__cibil-edit { + font-size: 11px; + font-weight: 600; + color: #1d4ed8; + cursor: pointer; + } + + &__footer { + background: white; + border: 1px solid #e2e8f0; + border-radius: 9px; + padding: 10px 14px; + display: flex; + align-items: center; + justify-content: space-between; + } + + &__footer-note { + font-size: 11px; + color: #64748b; + } + + &__footer-actions { + display: flex; + gap: 6px; + } + + &__submit { + background: #059669 !important; + color: white !important; + font-weight: 600; + + i { + margin-right: 4px; + } + } +} diff --git a/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts new file mode 100644 index 000000000..3d57a6eb4 --- /dev/null +++ b/src/app/products/loan-interest-rate-register/edit-register/edit-register.component.ts @@ -0,0 +1,196 @@ +/** + * 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, ElementRef, OnInit, ViewChild, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatNativeDateModule } from '@angular/material/core'; +import { RateRegisterService } from '../rate-register.service'; +import { RateCell, RateChartRow, RateRegister, SlabColumn } from '../models/rate-register.model'; + +/** + * Grid editor for one Loan Interest Rate Register — Excel-shaped: 34 products as rows and 10 + * amount slabs as columns. Cells can hold a plain rate, N.A., or a formula ("1% + FD"). All + * data today comes from {@link RateRegisterService}; wire-up to the real API comes later. + */ +@Component({ + selector: 'mifosx-edit-register', + templateUrl: './edit-register.component.html', + styleUrls: ['./edit-register.component.scss'], + imports: [ + CommonModule, + FormsModule, + RouterLink, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatIconModule, + MatDatepickerModule, + MatNativeDateModule + ] +}) +export class EditRegisterComponent implements OnInit { + private readonly service = inject(RateRegisterService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + + register!: RateRegister; + slabs: SlabColumn[] = []; + rows: RateChartRow[] = []; + + /** The cell currently being edited (row/column indices into the grid), or null when nothing is open. */ + editing: { row: number; col: number } | null = null; + editValue = ''; + + @ViewChild('cellInput') cellInput?: ElementRef; + + ngOnInit(): void { + const idParam = this.route.snapshot.queryParamMap.get('id'); + const id = idParam ? Number(idParam) : null; + this.service.list().subscribe((all) => { + this.register = (id ? all.find((r) => r.id === id) : undefined) ?? this.service.getActiveOrFirstDraft(); + this.slabs = this.register.slabs; + this.rows = this.register.rows; + }); + } + + /** Formatted display for a cell. */ + display(cell: RateCell): string { + if (cell === null) { + return 'N.A.'; + } + if (typeof cell === 'string') { + return cell; + } + return cell.toFixed(2); + } + + isFormulaRow(row: RateChartRow): boolean { + return !!row.isFormula; + } + + isNa(cell: RateCell): boolean { + return cell === null; + } + + /** Click on a cell → open it as an editable input, pre-filled with the current value. */ + startEdit(rowIdx: number, colIdx: number, current: RateCell): void { + this.editing = { row: rowIdx, col: colIdx }; + if (current === null) { + this.editValue = ''; + } else if (typeof current === 'string') { + this.editValue = current; + } else { + this.editValue = current.toString(); + } + // Let the view render the input, then focus and select its contents. + setTimeout(() => { + const el = this.cellInput?.nativeElement; + if (el) { + el.focus(); + el.select(); + } + }, 0); + } + + isEditing(rowIdx: number, colIdx: number): boolean { + return !!this.editing && this.editing.row === rowIdx && this.editing.col === colIdx; + } + + /** + * Save the cell: + * - empty / "N.A." / "NA" → the slab is marked unavailable + * - value with letters, "%" or "+" → persisted as a formula string ("1% + FD") + * - plain number → stored as a number so it formats as a % + */ + saveEdit(rowIdx: number, colIdx: number): void { + if (!this.editing) { + return; + } + const raw = this.editValue.trim(); + let next: RateCell; + if (!raw || raw.toLowerCase() === 'n.a.' || raw.toLowerCase() === 'na') { + next = null; + } else if (/[a-zA-Z+%]/.test(raw)) { + next = raw; + } else { + const num = Number(raw); + next = Number.isFinite(num) ? num : this.rows[rowIdx].rates[colIdx]; + } + this.rows[rowIdx].rates[colIdx] = next; + this.rows[rowIdx].isFormula = this.rows[rowIdx].rates.some((c) => typeof c === 'string'); + this.editing = null; + } + + cancelEdit(): void { + this.editing = null; + } + + /** Append a new empty product row at the bottom of the grid. */ + addProduct(): void { + const name = window.prompt('New product name', 'New product'); + if (!name) { + return; + } + const next = this.rows.length > 0 ? Math.max(...this.rows.map((r) => r.srNo)) + 1 : 1; + this.rows.push({ + srNo: next, + particulars: name.trim(), + rates: Array(this.slabs.length).fill(null), + isFormula: false + }); + } + + /** Append a new amount slab column; every existing row grows by one blank cell. */ + addSlab(): void { + const label = window.prompt('Slab label (e.g. "Upto 50,00,000" or "> 50,00,000")', 'New slab'); + if (!label) { + return; + } + const amountText = window.prompt('Amount upper bound in ₹ (leave blank for no cap)', ''); + const amountUpto = amountText && amountText.trim() ? Number(amountText) : null; + this.slabs.push({ + key: `s_${Date.now()}`, + label: label.trim(), + amountUpto: amountUpto !== null && !Number.isFinite(amountUpto) ? null : amountUpto + }); + this.rows.forEach((r) => r.rates.push(null)); + } + + editCibil(): void { + this.router.navigate(['/products/loan-interest-rate-register/cibil'], { queryParams: { id: this.register.id } }); + } + + backToList(): void { + this.router.navigate(['/products/loan-interest-rate-register']); + } + + submitForApproval(): void { + this.register.status = 'Active'; + this.router.navigate(['/products/loan-interest-rate-register']); + } + + statusChipClass(): string { + switch (this.register?.status) { + case 'Active': + return 'chip chip--active'; + case 'Draft': + return 'chip chip--draft'; + default: + return 'chip chip--expired'; + } + } +} diff --git a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.html b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.html new file mode 100644 index 000000000..76909d598 --- /dev/null +++ b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.html @@ -0,0 +1,83 @@ + + +
+
+
+
+

Loan Interest Rate Register

+

Central CIBIL-based rate chart used across all loan products

+
+ + +
+ +
+
+
+
ACTIVE REGISTER
+
{{ active?.name ?? 'None' }}
+
Effective {{ activeWindow() }}
+
+
+
SLABS
+
10 amount slabs
+
₹10k → > ₹25L
+
+
+
CIBIL ADJUSTMENTS
+
{{ active?.cibilSlabs?.length ?? 0 }} slabs
+
+1.00% → −0.20%
+
+
+ +
+
+
All registers
+
+ + + + + + + + + + + + + + + + + + + + + + + +
NameEffective fromEffective toStatusCreated byLast modified
+
{{ r.name }}
+
{{ r.note }}
+
{{ r.effectiveFrom | date: 'dd MMM yyyy' }}{{ r.effectiveTo ? (r.effectiveTo | date: 'dd MMM yyyy') : '—' }} + {{ r.status === 'Active' ? '● Active' : r.status }} + {{ r.createdBy }}{{ r.lastModified | date: 'dd MMM yyyy' }}{{ r.status === 'Draft' ? 'Edit · Submit' : 'View' }}
+
+
Showing 1–{{ registers.length }} of {{ registers.length }}
+
+
+
+
diff --git a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss new file mode 100644 index 000000000..1b930acd0 --- /dev/null +++ b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.scss @@ -0,0 +1,212 @@ +/** + * 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/. + */ + +.lirr { + background: #f8fafc; + min-height: calc(100vh - 160px); + + &__header { + display: flex; + align-items: center; + gap: 14px; + padding: 22px 32px 14px; + background: white; + border-bottom: 1px solid #e2e8f0; + } + + &__header-icon { + width: 40px; + height: 40px; + border-radius: 9px; + background: #eff6ff; + color: #1d4ed8; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + } + + &__header-text { + flex: 1; + + h2 { + margin: 0; + font-size: 20px; + font-weight: 600; + } + + p { + margin: 3px 0 0; + font-size: 12px; + color: #64748b; + } + } + + &__excel-btn, + &__create-btn { + i { + margin-right: 6px; + } + } + + &__body { + padding: 20px 32px; + } + + &__kpi-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 12px; + margin-bottom: 18px; + } + + &__kpi { + background: white; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 14px 16px; + } + + &__kpi-label { + font-size: 11px; + color: #64748b; + font-weight: 600; + letter-spacing: 0.3px; + } + + &__kpi-value { + font-size: 18px; + font-weight: 600; + margin-top: 4px; + } + + &__kpi-sub { + font-size: 11px; + color: #64748b; + margin-top: 4px; + + &--ok { + color: #059669; + } + } + + &__table-card { + background: white; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; + } + + &__table-head { + padding: 12px 20px; + border-bottom: 1px solid #f1f5f9; + display: flex; + align-items: center; + } + + &__table-title { + font-size: 14px; + font-weight: 600; + } + + &__table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + thead { + background: #f8fafc; + color: #64748b; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.3px; + + th { + text-align: left; + padding: 10px 12px; + font-weight: 600; + + &:first-child { + padding-left: 20px; + } + } + } + + tbody tr { + border-top: 1px solid #f1f5f9; + cursor: pointer; + transition: background-color 0.1s; + + &:hover { + background: #f8fafc; + } + + td { + padding: 12px; + + &:first-child { + padding-left: 20px; + } + } + } + } + + &__row--active { + background: #f0fdf4; + } + + &__name { + font-weight: 600; + } + + &__name-sub { + font-size: 11px; + color: #64748b; + margin-top: 2px; + } + + &__row-actions { + text-align: right; + color: #64748b; + padding-right: 20px !important; + } + + &__table-foot { + padding: 10px 20px; + border-top: 1px solid #f1f5f9; + font-size: 12px; + color: #64748b; + } +} + +.chip { + padding: 3px 10px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + + &--active { + background: #dcfce7; + color: #166534; + } + + &--draft { + background: #fef3c7; + color: #92400e; + } + + &--expired { + background: #f1f5f9; + color: #475569; + } + + &--scheduled { + background: #e0e7ff; + color: #3730a3; + } +} diff --git a/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts new file mode 100644 index 000000000..409735ebf --- /dev/null +++ b/src/app/products/loan-interest-rate-register/loan-interest-rate-register.component.ts @@ -0,0 +1,78 @@ +/** + * 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 { DatePipe } from '@angular/common'; +import { Router } from '@angular/router'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { RateRegisterService } from './rate-register.service'; +import { RateRegister } from './models/rate-register.model'; + +/** + * List screen for the central Loan Interest Rate Register — three KPI cards followed by the + * registers table (name, effective window, status, author, last modified). + */ +@Component({ + selector: 'mifosx-loan-interest-rate-register', + templateUrl: './loan-interest-rate-register.component.html', + styleUrls: ['./loan-interest-rate-register.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + DatePipe + ] +}) +export class LoanInterestRateRegisterComponent implements OnInit { + private readonly service = inject(RateRegisterService); + private readonly router = inject(Router); + + registers: RateRegister[] = []; + active?: RateRegister; + + ngOnInit(): void { + this.service.list().subscribe((rows) => { + this.registers = rows; + this.active = rows.find((r) => r.status === 'Active'); + }); + } + + edit(register: RateRegister): void { + this.router.navigate(['/products/loan-interest-rate-register/edit'], { queryParams: { id: register.id } }); + } + + create(): void { + this.router.navigate(['/products/loan-interest-rate-register/edit']); + } + + statusChipClass(status: RateRegister['status']): string { + switch (status) { + case 'Active': + return 'chip chip--active'; + case 'Draft': + return 'chip chip--draft'; + case 'Expired': + return 'chip chip--expired'; + default: + return 'chip chip--scheduled'; + } + } + + activeWindow(): string { + if (!this.active) { + return '—'; + } + return `${this.formatShort(this.active.effectiveFrom)} → ${this.formatShort(this.active.effectiveTo)}`; + } + + private formatShort(iso: string | null): string { + if (!iso) { + return '—'; + } + const d = new Date(iso); + return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' }); + } +} diff --git a/src/app/products/loan-interest-rate-register/models/rate-register.model.ts b/src/app/products/loan-interest-rate-register/models/rate-register.model.ts new file mode 100644 index 000000000..d1ffe6e02 --- /dev/null +++ b/src/app/products/loan-interest-rate-register/models/rate-register.model.ts @@ -0,0 +1,52 @@ +/** + * 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/. + */ + +export type RegisterStatus = 'Active' | 'Draft' | 'Expired' | 'Scheduled'; + +export interface SlabColumn { + key: string; + label: string; + amountUpto: number | null; +} + +/** A single rate cell: number for a %, string for a formula ("1% + FD"), null for N.A. */ +export type RateCell = number | string | null; + +export interface RateChartRow { + srNo: number; + particulars: string; + isFormula?: boolean; + rates: RateCell[]; +} + +export type RiskTier = 'Poor' | 'Fair' | 'Good' | 'V. Good' | 'Excellent'; + +export interface CibilSlab { + from: number; + to: number; + tier: RiskTier; + type: 'Flat'; + adjustmentPct: number; +} + +export interface RateRegister { + id: number; + name: string; + effectiveFrom: string; + effectiveTo: string | null; + currency: string; + status: RegisterStatus; + createdBy: string; + lastModified: string; + note?: string; + slabs: SlabColumn[]; + rows: RateChartRow[]; + cibilSlabs: CibilSlab[]; + fallbackNoCibilSlab: number; + fallbackBelowMin: 'use-min-slab' | 'block'; +} diff --git a/src/app/products/loan-interest-rate-register/rate-register.service.ts b/src/app/products/loan-interest-rate-register/rate-register.service.ts new file mode 100644 index 000000000..69453daec --- /dev/null +++ b/src/app/products/loan-interest-rate-register/rate-register.service.ts @@ -0,0 +1,457 @@ +/** + * 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 { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { CibilSlab, RateChartRow, RateRegister, SlabColumn } from './models/rate-register.model'; + +/** + * In-memory stand-in for the central Loan Interest Rate Register. + * + * Mirrors the Excel chart the bank's Board approves each quarter (34 products × 10 amount slabs). + * Backing API will land later; today the service only serves the demo data in memory so the + * screens can be driven end-to-end without a server. + */ +@Injectable({ providedIn: 'root' }) +export class RateRegisterService { + private readonly slabs: SlabColumn[] = [ + { key: 's1', label: 'Upto 10,000', amountUpto: 10000 }, + { key: 's2', label: 'Upto 25,000', amountUpto: 25000 }, + { key: 's3', label: 'Upto 1,00,000', amountUpto: 100000 }, + { key: 's4', label: 'Upto 2,00,000', amountUpto: 200000 }, + { key: 's5', label: 'Upto 5,00,000', amountUpto: 500000 }, + { key: 's6', label: 'Upto 10,00,000', amountUpto: 1000000 }, + { key: 's7', label: 'Upto 15,00,000', amountUpto: 1500000 }, + { key: 's8', label: 'Upto 20,00,000', amountUpto: 2000000 }, + { key: 's9', label: 'Upto 25,00,000', amountUpto: 2500000 }, + { key: 's10', label: '> 25,00,000', amountUpto: null } + ]; + + // Values taken from the bank's approved Interest Rate of Loans register. + private readonly rows: RateChartRow[] = [ + this.row(1, 'Consumer Loan', [ + 18, + 18, + 15, + 13, + null, + null, + null, + null, + null, + null + ]), + this.row( + 2, + 'Self Employment Loan', + [ + 18, + 18, + 15, + 13, + 12.5, + 12, + 11.5, + 11, + 10.5, + 10.5 + ] + ), + this.row( + 3, + 'Business Term Loan for running', + [ + 18, + 18, + 15, + 13, + 12.5, + 11, + 10.5, + 10, + 9.5, + 9.5 + ] + ), + this.row( + 4, + 'Cash Credit', + [ + 18, + 18, + 15, + 13, + 13, + 11, + 10.5, + 10, + 9.5, + 9.5 + ] + ), + this.row( + 5, + 'Limit (OD) on property — MSME', + [ + 18, + 18, + 15, + 13, + 13, + 11, + 10.5, + 10, + 9.5, + 9.5 + ] + ), + this.row( + 6, + 'Limit (OD) on property — Personal', + [ + 18, + 18, + 15, + 14, + 13.5, + 12, + 11.5, + 11, + 10.5, + 10.5 + ] + ), + this.row(7, 'House Purchase / Construction', [ + null, + null, + 15, + 12, + 12, + 9.25, + 8.75, + 8.25, + 8, + 8 + ]), + this.row(8, 'House Repairing / Renovation', [ + null, + null, + 15, + 14, + 14, + 13, + 12, + 11, + 10.5, + 10 + ]), + this.row( + 9, + 'Commercial Real Estate Loan', + [ + 18, + 18, + 15, + 13, + 12.5, + 12, + 11, + 10.5, + 10.5, + 10 + ] + ), + this.row(10, 'Loan against Jewellery', this.flat(10.8)), + this.row(11, 'Loan against Warehouse Receipt (A)', this.flat(13)), + this.row( + 12, + 'Loan against Warehouse Receipt (B)', + [ + 10.5, + 10.5, + 10.5, + 10.5, + 10.5, + 10.5, + 10, + 10, + 10, + 9 + ] + ), + this.row(13, 'Agriculture Loan', [ + null, + null, + 15, + 12, + 12, + 12, + 11.75, + 11.5, + 11.25, + 11 + ]), + this.row(14, 'Personal Loan (Unsecured)', [ + 18, + 18, + 16, + 15, + null, + null, + null, + null, + null, + null + ]), + this.row( + 15, + 'Personal Loan (Secured / Property)', + [ + 18, + 18, + 16, + 15, + 15, + 13, + 12, + 11.5, + 11, + 11 + ] + ), + this.row(16, 'Two Wheeler Loan', this.flat(13)), + this.row(17, 'Car Loan (4-Wheel Domestic)', [ + null, + null, + 10, + 9.5, + 9.5, + 8.75, + 8.5, + 8.35, + 8.25, + 8.25 + ]), + this.row(18, 'Old Car Loan (4-Wheel Domestic)', [ + null, + null, + 13, + 12.5, + 12.5, + 12, + 11.5, + 11, + 10.5, + 10.5 + ]), + this.row(19, 'Vehicle Loan (Commercial & Three-wheeler)', [ + null, + null, + 13, + 12, + 12, + 10.5, + 10, + 10, + 10, + 10 + ]), + this.row(20, 'Old Vehicle Loan (Commercial)', [ + null, + null, + 15, + 14, + 14, + 13.5, + 13.5, + 13, + 12.5, + 12.5 + ]), + this.row( + 21, + 'Machinery & Plant Loan', + [ + 18, + 18, + 15, + 13, + 12.5, + 12, + 11, + 10.5, + 10.5, + 10 + ] + ), + this.row( + 22, + 'Office Chamber / Equipment Loan', + [ + 18, + 18, + 15, + 13, + 12.5, + 12, + 11, + 10.5, + 10.5, + 10 + ] + ), + this.formulaRow(23, 'Loan against F.D.R.', [ + '1% + FD', + '1% + FD', + '1% + FD', + '1% + FD', + '1% + FD', + '1% + FD', + '0.75% + FD', + '0.50% + FD', + '0.50% + FD', + '0.50% + FD' + ]), + this.row(24, 'Loan against Daily Deposit & Gold', this.flat(10)), + this.formulaRow(25, 'Loan against Recurring Deposit', Array(10).fill('1% + RD')), + this.row(26, 'Loan against Staff Service Fund', this.flat(7)), + this.row(27, 'Over Draft A/c', this.flat(null)), + this.row(28, 'Loan on LIC Policy', this.flat(null)), + this.row(29, 'Loan on KVP / NSC', this.flat(null)), + this.row(30, 'Bill Discount', this.flat(null)), + this.row(31, 'Temporary Limit', this.flat(15)), + this.row(32, 'Bank Guarantee (commission)', this.flat(0.25)), + this.row(33, 'Sugam Vyapar Loan', this.flat(null)), + this.row(34, 'OD to Staff (upto 2 lakh)', this.flat(null)) + ]; + + private readonly cibilSlabs: CibilSlab[] = [ + { from: 0, to: 649, tier: 'Poor', type: 'Flat', adjustmentPct: 1.0 }, + { from: 650, to: 749, tier: 'Fair', type: 'Flat', adjustmentPct: 0.5 }, + { from: 750, to: 799, tier: 'Good', type: 'Flat', adjustmentPct: 0 }, + { from: 800, to: 850, tier: 'V. Good', type: 'Flat', adjustmentPct: -0.1 }, + { from: 851, to: 900, tier: 'Excellent', type: 'Flat', adjustmentPct: -0.2 } + ]; + + private readonly registers: RateRegister[] = [ + { + id: 1001, + name: 'Q1 2027 Rates', + effectiveFrom: '2027-01-01', + effectiveTo: '2027-03-31', + currency: 'INR', + status: 'Draft', + createdBy: 'Sonali', + lastModified: '2026-10-01', + note: 'Scheduled · future effective', + slabs: this.slabs, + rows: this.rows, + cibilSlabs: this.cibilSlabs, + fallbackNoCibilSlab: 2, + fallbackBelowMin: 'block' + }, + { + id: 1002, + name: 'Interest Rate of Loans — Oct 2026', + effectiveFrom: '2026-10-01', + effectiveTo: '2026-12-31', + currency: 'INR', + status: 'Active', + createdBy: 'Admin', + lastModified: '2026-09-25', + note: 'Approved by Board on 25 Sep', + slabs: this.slabs, + rows: this.rows, + cibilSlabs: this.cibilSlabs, + fallbackNoCibilSlab: 2, + fallbackBelowMin: 'block' + }, + { + id: 1003, + name: 'Q3 2026 Rates', + effectiveFrom: '2026-07-01', + effectiveTo: '2026-09-30', + currency: 'INR', + status: 'Expired', + createdBy: 'Admin', + lastModified: '2026-06-15', + slabs: this.slabs, + rows: this.rows, + cibilSlabs: this.cibilSlabs, + fallbackNoCibilSlab: 2, + fallbackBelowMin: 'block' + }, + { + id: 1004, + name: 'Q2 2026 Rates', + effectiveFrom: '2026-04-01', + effectiveTo: '2026-06-30', + currency: 'INR', + status: 'Expired', + createdBy: 'Admin', + lastModified: '2026-03-10', + slabs: this.slabs, + rows: this.rows, + cibilSlabs: this.cibilSlabs, + fallbackNoCibilSlab: 2, + fallbackBelowMin: 'block' + }, + { + id: 1005, + name: 'Q1 2026 Rates', + effectiveFrom: '2026-01-01', + effectiveTo: '2026-03-31', + currency: 'INR', + status: 'Expired', + createdBy: 'Admin', + lastModified: '2025-12-20', + slabs: this.slabs, + rows: this.rows, + cibilSlabs: this.cibilSlabs, + fallbackNoCibilSlab: 2, + fallbackBelowMin: 'block' + } + ]; + + list(): Observable { + return of(this.registers); + } + + get(id: number): Observable { + return of(this.registers.find((r) => r.id === id)); + } + + /** The one shown by default on list → edit navigation. */ + getActiveOrFirstDraft(): RateRegister { + return ( + this.registers.find((r) => r.status === 'Draft') ?? + this.registers.find((r) => r.status === 'Active') ?? + this.registers[0] + ); + } + + getSlabColumns(): SlabColumn[] { + return this.slabs; + } + + getCibilSlabs(): CibilSlab[] { + return this.cibilSlabs; + } + + private row(srNo: number, particulars: string, rates: (number | null)[]): RateChartRow { + return { srNo, particulars, rates }; + } + + private formulaRow(srNo: number, particulars: string, rates: string[]): RateChartRow { + return { srNo, particulars, isFormula: true, rates }; + } + + private flat(value: number | null): (number | null)[] { + return Array(10).fill(value); + } +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-advance-limit-tenure-step/loan-product-advance-limit-tenure-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-advance-limit-tenure-step/loan-product-advance-limit-tenure-step.component.ts index b35557303..ddb60f62b 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-advance-limit-tenure-step/loan-product-advance-limit-tenure-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-advance-limit-tenure-step/loan-product-advance-limit-tenure-step.component.ts @@ -58,7 +58,7 @@ export class LoanProductAdvanceLimitTenureStepComponent { readonly appropriationOrderOptions = [ { id: 'charges_penal_interest_principal', label: 'Charges → Penal → Interest → Principal' }, - { id: 'penal_interest_principal_charges', label: 'Penal → Interest → Principal → Charges' } + { id: 'principal_interest_penal_charges', label: 'Principal → Interest → Penal → Charges' } ]; readonly yesNoOptions = [ diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html index 62b792538..e35ab7664 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html @@ -103,8 +103,8 @@ {{ 'labels.inputs.Start Date' | translate }} {{ 'labels.inputs.Close Date' | translate }} + +