diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html index 2cf0ae0b6..cf0f71695 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html @@ -106,20 +106,38 @@
2 Ownership Share + @if (maxShare < 100) { + Max available: {{ maxShare }}% + }
Share Percentage - + % - Enter a value between 1 and 100 + @if (form.get('sharePercentage').hasError('max')) { + Maximum available is {{ maxShare }}% + } @else { + Enter a value between 1 and {{ maxShare }} + }
@for (hint of [25, 33, 50, 100]; track hint) { - + } + } + @if (maxShare > 0 && ![25, 33, 50, 100].includes(maxShare)) { + }
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss index 2028c02af..9d03a4c76 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss @@ -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 ─────────────────────────────────────────────────────────────────── diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts index 81cfb1945..407cfe4bd 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts @@ -32,16 +32,21 @@ export class AddRelatedClientDialogComponent implements OnDestroy { private fb = inject(UntypedFormBuilder); private clientsService = inject(ClientsService); dialogRef = inject(MatDialogRef); - 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], diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 437f00aa2..e99add781 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -259,6 +259,7 @@ color="primary" type="button" class="soa-add-btn" + [disabled]="totalJointSharePercentage >= 100" (click)="openAddJointHolderDialog()" > person_add Add Holder @@ -761,6 +762,7 @@ color="primary" type="button" class="ni-add-btn" + [disabled]="totalSharePercentage >= 100" (click)="openAddRelatedClientDialog()" > person_add Add Partner diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index 196fea25d..08ff06782 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -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) { diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html index f78358d85..a505a994f 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html @@ -74,6 +74,7 @@ diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts index 9365dae78..3f32de9ea 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts @@ -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( diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html index a85665e72..8e6b1e498 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html @@ -370,6 +370,7 @@ color="primary" type="button" class="soa-add-btn" + [disabled]="totalJointSharePercentage >= 100" (click)="openAddJointHolderDialog()" > person_add Add Holder diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts index f7be4a3a0..d2562d4de 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts @@ -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) { diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html index 8940bda46..add375f1d 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html @@ -8,7 +8,7 @@
- @if (fixedDepositsAccountProductTemplate?.accountChart) { + @if (interestRateChartData.length > 0) {
@@ -163,6 +163,18 @@
+ +
+ +
+
+
+ + @if (slabs(fdType).length === 0) { +

No slabs defined. Click "Add Slab" to begin.

+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ row.get('srNo').value }}Description + + + + Period Type + + + @for (pt of periodTypes; track pt.id) { + {{ pt.label }} + } + + + From + + + + To + + + + Annual Rate (%) + + + + Senior Citizen (%) + + + + + +
+
+ } + + +
+ + New Version Label + + Version label is required. + + + Remarks (optional) + + +
+ +
+ +
+ } @else { + +
+
+

CBS Base Interest Rate — Save Preview ({{ fdType }})

+

+ Review the change before confirming. The current active version will be + deactivated, and the new version will be + activated. +

+
+ +
+ +
+
+ Currently Active + → Will be DEACTIVATED +
+ @if (activeRates[fdType]) { +

{{ activeRates[fdType].versionLabel }}

+ @if (activeRates[fdType].remarks) { +

{{ activeRates[fdType].remarks }}

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ s.srNo }}Description{{ s.description }}Period{{ getPeriodLabel(s.periodTypeEnum) }}From{{ s.fromPeriod }}To{{ s.toPeriod ?? '∞' }}Rate (%){{ s.annualInterestRate | number: '1.2-2' }}Senior (%){{ s.seniorCitizenRate | number: '1.2-2' }}
+ } @else { +

No active version currently exists.

+ } +
+ + +
+
+ New Version + → Will be ACTIVATED +
+

{{ forms[fdType].value.versionLabel }}

+ @if (forms[fdType].value.remarks) { +

{{ forms[fdType].value.remarks }}

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ s.srNo }}Description{{ s.description }}Period{{ getPeriodLabel(s.periodTypeEnum) }}From{{ s.fromPeriod }}To{{ s.toPeriod ?? '∞' }}Rate (%){{ s.annualInterestRate | number: '1.2-2' }}Senior (%){{ s.seniorCitizenRate | number: '1.2-2' }}
+
+
+ +
+ + +
+
+ } + + @if (saveMessage[fdType]) { +

{{ saveMessage[fdType] }}

+ } + + +
+
+
+

CBS Base Interest Rate — Version History

+

All past versions for {{ fdType }} deposit type.

+
+
+ + @if (isLoading) { +

Loading history...

+ } @else if (historyForType(fdType).length === 0) { +

No history found.

+ } @else { + + @for (version of historyForType(fdType); track version.id) { + + + + + {{ version.versionLabel }} + + {{ version.isActive ? 'Active' : 'Inactive' }} + + + + + From {{ version.effectiveFromDate }} + To {{ version.effectiveToDate || 'Current' }} + + + + @if (version.remarks) { +

{{ version.remarks }}

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ s.srNo }}Description{{ s.description }}Period{{ getPeriodLabel(s.periodTypeEnum) }}From{{ s.fromPeriod }}To{{ s.toPeriod ?? '∞' }}Annual Rate (%){{ s.annualInterestRate | number: '1.2-2' }}Senior Citizen (%){{ s.seniorCitizenRate | number: '1.2-2' }}
+
+ } +
+ } +
+
+ + } + +
diff --git a/src/app/products/fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component.scss b/src/app/products/fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component.scss new file mode 100644 index 000000000..e842fac82 --- /dev/null +++ b/src/app/products/fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component.scss @@ -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; +} diff --git a/src/app/products/fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component.ts b/src/app/products/fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component.ts new file mode 100644 index 000000000..9a1032dd7 --- /dev/null +++ b/src/app/products/fixed-deposit-products/base-fd-interest-rate-chart/base-fd-interest-rate-chart.component.ts @@ -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 = {}; + activeRates: Record = {}; + historyRates: any[] = []; + + isLoading = false; + isSaving: Record = {}; + saveMessage: Record = {}; + showPreview: Record = {}; + + 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) + ] + }); + } +} diff --git a/src/app/products/products-routing.module.ts b/src/app/products/products-routing.module.ts index c1a923d76..034086845 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -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, diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html index b68ad5f2f..3345cdb55 100644 --- a/src/app/products/products.component.html +++ b/src/app/products/products.component.html @@ -87,6 +87,25 @@
+
+ +
+ +
+
+

CBS Base Interest Rate

+

+ Manage and activate CBS base interest rate slabs used for Fixed and Recurring Deposit products +

+
+ +
+
+
- -
- - - @if (recurringDepositProductsTemplate?.chartTemplate) { - - - -
- - @if (saveMessage) { - {{ saveMessage }} - } -
- } @else { -

Loading CBS base chart...

- } - -
-
-
-

Chart History

-

Previous CBS base chart versions are listed here for comparison and audit.

-
-
- - @if (isHistoryLoading) { -

Loading history...

- } @else if (rdInterestRateHistory.length === 0) { -

No history found for the CBS base chart.

- } @else { - - @for (version of rdInterestRateHistory; track version.id) { - - - - - {{ version.versionLabel }} - - {{ version.isActive ? 'Active' : 'Inactive' }} - - - - - From {{ version.effectiveFromDate }} - To {{ version.effectiveToDate || 'Current' }} - - - - @if (version.remarks) { -

{{ version.remarks }}

- } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ slab.srNo }}Description{{ slab.description }}From{{ slab.fromPeriod }}To - {{ slab.toPeriod !== null && slab.toPeriod !== undefined ? slab.toPeriod : 'N/A' }} - Period Type{{ slab.periodTypeLabel }}Annual Rate (%){{ slab.annualInterestRate | number: '1.2-2' }}
-
- } -
- } -
-
diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss deleted file mode 100644 index 00de402dc..000000000 --- a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss +++ /dev/null @@ -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%; -} diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts deleted file mode 100644 index 811810cfc..000000000 --- a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts +++ /dev/null @@ -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; - } - }); - } -} diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index 5ac5521e6..5d2511450 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -226,6 +226,7 @@ color="primary" type="button" class="soa-add-btn" + [disabled]="totalJointSharePercentage >= 100" (click)="openAddJointHolderDialog()" > person_add Add Holder diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index ac67782d2..93cf17372 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -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) { diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 82fcafb96..c8f9fe9b3 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260520', - 'hash': 'bf3e26a0' + 'version': '260521', + 'hash': '846b2484' }, 'allow_switching_backend_instance': true };