From 59e1eeacc387b8137b70df1ef1133cf126cced8a Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 28 Sep 2026 13:02:38 +0530 Subject: [PATCH 1/4] fix(accounting): show latest journal entries first by default The table fell back to the backend's own default order (entry_date, id ascending) until a maker clicked a column header, so the oldest entries sat at the top on every fresh load. Defaults the table's sort to id descending instead. Co-Authored-By: Claude Sonnet 5 --- .../search-journal-entry/search-journal-entry.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/accounting/search-journal-entry/search-journal-entry.component.html b/src/app/accounting/search-journal-entry/search-journal-entry.component.html index 6938ce4bd..f04105114 100644 --- a/src/app/accounting/search-journal-entry/search-journal-entry.component.html +++ b/src/app/accounting/search-journal-entry/search-journal-entry.component.html @@ -539,7 +539,7 @@
- + # From 8d0d8822f0fc26c893644113a067488d8e1a2456 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 28 Sep 2026 13:03:01 +0530 Subject: [PATCH 2/4] feat(fd): convert Premature Close to a dialog, with its full calculation shown Was a full-page form at a dedicated route, reached by navigating away from the account -- now a popup dialog in the same visual language as the Payment/Renew dialog, just in this Danger Zone action's own red accent. Also fixes a real gap in the form it replaces: the premature amount/action options never loaded until a maker happened to touch the date picker, since the calculation was only ever triggered on value change, never on open. Added a breakdown panel showing exactly how the shown Maturity Amount was reached -- Deposit Amount, Contracted Interest Rate, Pre-Closure Penalty (when applicable), and the resulting Interest Earned -- using data the account-view page already had, since the calculation endpoint itself only ever returns the final number. Removed the now-dead full-page component/route target it replaces. Co-Authored-By: Claude Sonnet 5 --- .../premature-close-dialog.component.html | 194 +++++++++ .../premature-close-dialog.component.scss | 388 ++++++++++++++++++ .../premature-close-dialog.component.ts | 218 ++++++++++ .../fixed-deposit-account-view.component.ts | 39 +- ...ed-deposits-account-actions.component.html | 3 - ...ixed-deposits-account-actions.component.ts | 4 - ...lose-fixed-deposits-account.component.html | 105 ----- ...lose-fixed-deposits-account.component.scss | 11 - ...-close-fixed-deposits-account.component.ts | 179 -------- .../fixed-deposits/fixed-deposits.module.ts | 2 - 10 files changed, 836 insertions(+), 307 deletions(-) create mode 100644 src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html create mode 100644 src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss create mode 100644 src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts delete mode 100644 src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html delete mode 100644 src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss delete mode 100644 src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html new file mode 100644 index 000000000..018b95331 --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html @@ -0,0 +1,194 @@ + + +
+
+
+ +
+

{{ 'labels.heading.Premature Close' | translate }}

+

+ Close {{ data.accountNo ?? 'this account' }} before maturity -- interest is recalculated as of the date below, + usually at a lower, penalty rate. +

+
+
+ +
+ +
+ @if (loadFailed) { + + } @else { +
+ + {{ 'labels.inputs.Premature Close Date' | translate }} + + + + @if (prematureCloseForm.controls.closedOnDate.hasError('required')) { + + {{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + @if (loadingAmount) { +
+ + Calculating premature-closure amount... +
+ } @else { + @if (maturityAmount !== null && maturityAmount !== undefined) { +
+ @if (data.depositAmount) { +
+ Deposit Amount + {{ data.depositAmount | formatNumber }} +
+ } + @if (data.nominalAnnualInterestRate) { +
+ Contracted Interest Rate + {{ data.nominalAnnualInterestRate }}% +
+ } + @if (data.preClosurePenalApplicable) { +
+ Pre-Closure Penalty (on {{ data.preClosurePenalInterestOnType?.value }}) + − {{ data.preClosurePenalInterest }}% +
+ } + @if (interestEarned !== null) { +
+ Interest Earned (to {{ 'labels.inputs.Premature Close Date' | translate }}) + + {{ interestEarned | formatNumber }} +
+ } +
+ {{ 'labels.inputs.Maturity Amount' | translate }} + {{ maturityAmount | formatNumber }} +
+
+ } + + +
+ @for (closureOption of onAccountClosureOptions; track closureOption) { + + } +
+ @if ( + prematureCloseForm.controls.onAccountClosureId?.hasError('required') && + prematureCloseForm.controls.onAccountClosureId?.touched + ) { +
+ {{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} +
+ } + + @if (noDestinationAccounts) { + + } + + @if (prematureCloseForm.contains('toSavingsAccountId')) { + + {{ 'labels.inputs.Transfer to Savings' | translate }} + + @for (account of savingsAccountsData; track account) { + + {{ account.accountNo }} + + } + + @if (prematureCloseForm.controls.toSavingsAccountId?.hasError('required')) { + + {{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + } + + @if (prematureCloseForm.contains('transferDescription')) { + + {{ 'labels.inputs.Transfer Description' | translate }} + + + } + + @if (prematureCloseForm.contains('note')) { + + {{ 'labels.inputs.Note' | translate }} + + + } + } + + @if (submitError) { + + } +
+ } +
+ +
+ + +
+
diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss new file mode 100644 index 000000000..289c1380e --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss @@ -0,0 +1,388 @@ +/** + * 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/. + */ + +/** + * Premature Close Dialog Styles + * CBSNexora Core Banking System + * + * Same shape/spacing/typography as the Payment dialog (.pmt__*), in this Danger Zone action's own red + * accent on the header and Confirm button, rather than Payment's blue -- an irreversible, usually + * penalty-bearing action reads differently from a routine one, everywhere else stays identical so the + * two dialogs feel like the same product. + */ + +.pcd { + display: flex; + flex-direction: column; + height: 100%; + max-height: 92vh; + box-sizing: border-box; + font-family: + 'DM Sans', + system-ui, + -apple-system, + sans-serif; + color: #0f172a; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 24px 48px -12px rgb(15 23 42 / 25%); +} + +.pcd__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 26px 30px; + background: linear-gradient(135deg, #7f1d1d 0%, #b91c1c 55%, #dc2626 100%); + color: #fff; + flex-shrink: 0; + position: relative; + overflow: hidden; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 88% -20%, rgb(255 255 255 / 22%), transparent 55%); + pointer-events: none; + } + + &-left { + display: flex; + align-items: center; + gap: 16px; + position: relative; + z-index: 1; + } + + &-ico { + width: 48px; + height: 48px; + border-radius: 14px; + background: rgb(255 255 255 / 16%); + border: 1px solid rgb(255 255 255 / 25%); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + color: #fff; + flex-shrink: 0; + } + + &-text { + display: flex; + flex-direction: column; + } + + &-close { + position: relative; + z-index: 1; + background: rgb(255 255 255 / 14%); + border: none; + color: #fff; + width: 34px; + height: 34px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover:not(:disabled) { + background: rgb(255 255 255 / 28%); + transform: rotate(90deg); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } +} + +.pcd__title { + margin: 0; + font-size: 20px; + font-weight: 800; + color: #fff; + line-height: 1.25; + letter-spacing: -0.01em; +} + +.pcd__subtitle { + margin: 5px 0 0; + font-size: 13px; + color: rgb(255 255 255 / 88%); + line-height: 1.4; +} + +.pcd__content { + padding: 26px 30px !important; + overflow-y: auto; + flex: 1 1 auto; + background: #fbfcfe; + + mat-form-field { + width: 100%; + } +} + +.pcd__loading { + display: flex; + align-items: center; + gap: 12px; + color: #64748b; + font-size: 13px; + padding: 14px 0 18px; +} + +.pcd__preview { + display: flex; + flex-direction: column; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 4px 18px; + margin: 0 0 18px; + + &-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 11px 0; + border-bottom: 1px solid #f1f5f9; + + &:last-child { + border-bottom: none; + } + + &--highlight { + .pcd__preview-value { + color: #b91c1c; + font-weight: 800; + } + } + + &--total { + margin: 0 -18px; + padding: 14px 18px; + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); + border-bottom: none; + border-radius: 0 0 12px 12px; + + .pcd__preview-label { + color: #b91c1c; + font-weight: 700; + } + + .pcd__preview-value { + color: #0f172a; + font-size: 18px; + font-weight: 800; + } + } + } + + &-label { + font-size: 12.5px; + color: #64748b; + font-weight: 600; + } + + &-value { + font-family: 'JetBrains Mono', 'DM Sans', monospace; + font-size: 14px; + font-weight: 700; + color: #0f172a; + } +} + +.pcd__section-label { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #475569; + margin: 10px 0 12px; +} + +/* Action Options Grid - 2 Equal Columns */ +.pcd__options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; + margin-bottom: 18px; + + @media (width <= 560px) { + grid-template-columns: 1fr; + } +} + +.pcd__option { + display: flex; + align-items: flex-start; + gap: 14px; + text-align: left; + background: #fff; + border: 1.5px solid #e2e8f0; + border-radius: 14px; + padding: 16px; + cursor: pointer; + font: inherit; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + + &:hover { + border-color: #fca5a5; + background: #f8fafc; + box-shadow: 0 6px 16px rgb(15 23 42 / 6%); + transform: translateY(-1px); + } + + &--active { + border-color: #dc2626; + background: #fef2f2; + box-shadow: 0 0 0 1.5px #dc2626; + + .pcd__option-ico { + background: #dc2626; + color: #fff; + border-color: #dc2626; + } + } + + &-ico { + width: 38px; + height: 38px; + min-width: 38px; + border-radius: 10px; + background: #f1f5f9; + border: 1px solid #e2e8f0; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex-shrink: 0; + transition: all 0.2s ease; + } + + &-text { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + } + + &-title { + font-size: 14px; + font-weight: 700; + color: #0f172a; + line-height: 1.3; + } + + &-desc { + font-size: 12px; + color: #64748b; + line-height: 1.4; + } + + &-check { + color: #dc2626; + font-size: 17px; + position: absolute; + top: 14px; + right: 14px; + } +} + +.pcd__option-error { + color: #b91c1c; + font-size: 12px; + font-weight: 600; + margin: -10px 0 14px; +} + +.pcd__hint { + display: flex; + align-items: flex-start; + gap: 10px; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-left: 4px solid #0284c7; + border-radius: 10px; + color: #0369a1; + font-size: 13px; + line-height: 1.5; + padding: 12px 16px; + margin: 0 0 18px; + + &--warn { + background: #fffbeb; + border-color: #fde68a; + border-left-color: #d97706; + color: #b45309; + } +} + +.pcd__error { + display: flex; + align-items: flex-start; + gap: 10px; + background: #fef2f2; + border: 1px solid #fecaca; + border-left: 4px solid #ef4444; + border-radius: 10px; + color: #b91c1c; + font-size: 13px; + padding: 12px 16px; + margin: 10px 0; +} + +.pcd__actions { + padding: 18px 30px !important; + border-top: 1px solid #e2e8f0; + background: #fff; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 14px; + flex-shrink: 0; + + .pcd__btn-cancel { + border-radius: 10px; + font-weight: 600; + color: #475569; + } + + .pcd__btn-submit { + border-radius: 10px; + font-weight: 700; + padding: 0 26px; + height: 42px; + background: linear-gradient(135deg, #b91c1c 0%, #dc2626 100%); + color: #fff; + box-shadow: 0 8px 20px rgb(220 38 38 / 30%); + transition: all 0.2s ease; + + &:hover:not(:disabled) { + box-shadow: 0 10px 24px rgb(220 38 38 / 40%); + transform: translateY(-1px); + } + + &:disabled { + background: #cbd5e1; + box-shadow: none; + color: #64748b; + } + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts new file mode 100644 index 000000000..2122653e5 --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts @@ -0,0 +1,218 @@ +/** + * 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/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogRef, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; + +/** Custom Services */ +import { FixedDepositsService } from '../../../fixed-deposits.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * "Premature Close" dialog -- Active-only, no reinvest option (the deposit hasn't matured, so there's + * nothing to renew): pay out in cash, or transfer the pre-maturity balance to a savings account. Same + * `.pcd__*` visual language as the Payment dialog ({@link PaymentDialogComponent}), in this Danger Zone + * action's own red accent rather than Payment's blue, since it's an irreversible, usually + * penalty-bearing action rather than a routine one. + *

+ * The premature amount is date-dependent (unlike Payment's one-shot template load): every change to + * Closed On Date re-calls `calculatePrematureAmount`, same as the full-page form this replaces did. + */ +@Component({ + selector: 'mifosx-premature-close-dialog', + templateUrl: './premature-close-dialog.component.html', + styleUrls: ['./premature-close-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose, + CdkTextareaAutosize, + FaIconComponent, + FormatNumberPipe + ] +}) +export class PrematureCloseDialogComponent implements OnInit { + /** The `onAccountClosureId` codes Premature Close can offer -- same codes Payment/Close use. */ + private static readonly WITHDRAW_DEPOSIT_ID = 100; + private static readonly TRANSFER_TO_SAVINGS_ID = 200; + + private static readonly ACTION_ICONS: Record = { + [PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'money-bill-wave', + [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'wallet' + }; + + private static readonly ACTION_DESCRIPTIONS: Record = { + [PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'Pay out the full balance in cash', + [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'Move the full balance to another savings account' + }; + + dialogRef = inject>(MatDialogRef); + data: { + accountId: number | string; + accountNo?: string; + depositAmount?: number; + nominalAnnualInterestRate?: number; + preClosurePenalApplicable?: boolean; + preClosurePenalInterest?: number; + preClosurePenalInterestOnType?: { value: string }; + } = inject(MAT_DIALOG_DATA); + private formBuilder = inject(UntypedFormBuilder); + private fixedDepositsService = inject(FixedDepositsService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + prematureCloseForm: UntypedFormGroup; + onAccountClosureOptions: any[] = []; + savingsAccountsData: any[] = []; + maturityAmount: number = null; + loadingAmount = true; + loadFailed = false; + submitting = false; + submitError: string = null; + minDate = new Date(2000, 0, 1); + maxDate: Date; + + ngOnInit(): void { + this.maxDate = this.settingsService.businessDate; + this.prematureCloseForm = this.formBuilder.group({ + closedOnDate: [ + this.maxDate, + Validators.required + ] + }); + + this.prematureCloseForm + .get('closedOnDate') + .valueChanges.subscribe((date: Date) => this.calculatePrematureAmount(date)); + // Same date the form already starts with -- without this the amount/options never load unless the + // maker happens to touch the date picker, which the full-page form this replaces required too. + this.calculatePrematureAmount(this.maxDate); + } + + private calculatePrematureAmount(date: Date): void { + this.loadingAmount = true; + this.loadFailed = false; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const data = { + closedOnDate: this.dateUtils.formatDate(date, dateFormat), + dateFormat, + locale + }; + this.fixedDepositsService + .executeFixedDepositsAccountCommand(this.data.accountId as string, 'calculatePrematureAmount', data) + .subscribe({ + next: (response: any) => { + this.savingsAccountsData = response.savingsAccounts ?? []; + this.onAccountClosureOptions = response.onAccountClosureOptions ?? []; + this.maturityAmount = response.maturityAmount; + this.loadingAmount = false; + if (!this.prematureCloseForm.contains('onAccountClosureId')) { + this.prematureCloseForm.addControl('onAccountClosureId', new UntypedFormControl('', Validators.required)); + this.prematureCloseForm.addControl('note', new UntypedFormControl('')); + this.prematureCloseForm + .get('onAccountClosureId') + .valueChanges.subscribe((id: number) => this.onClosureTypeChange(id)); + } + }, + // Without this the spinner would run forever on a failed load, leaving Cancel as the only way out. + error: () => { + this.loadingAmount = false; + this.loadFailed = true; + } + }); + } + + /** Adds/removes the destination-account control to match what the chosen type needs. */ + private onClosureTypeChange(id: number): void { + if (id === PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID) { + if (!this.prematureCloseForm.contains('toSavingsAccountId')) { + this.prematureCloseForm.addControl('toSavingsAccountId', new UntypedFormControl('', Validators.required)); + this.prematureCloseForm.addControl('transferDescription', new UntypedFormControl('')); + } + } else { + this.prematureCloseForm.removeControl('toSavingsAccountId'); + this.prematureCloseForm.removeControl('transferDescription'); + } + } + + /** The gap between the deposit and the computed maturity amount -- the actual interest this premature + * closure earns, at whatever rate (contracted, or the lower pre-closure-penalty one below) applied. */ + get interestEarned(): number | null { + if (this.maturityAmount === null || this.maturityAmount === undefined || !this.data.depositAmount) { + return null; + } + return Number(this.maturityAmount) - Number(this.data.depositAmount); + } + + /** True when the chosen action needs a destination account but the customer has none to pick. */ + get noDestinationAccounts(): boolean { + return ( + !this.loadingAmount && + this.prematureCloseForm?.contains('toSavingsAccountId') && + this.savingsAccountsData.length === 0 + ); + } + + iconFor(id: number): string { + return PrematureCloseDialogComponent.ACTION_ICONS[id] ?? 'circle'; + } + + descriptionFor(id: number): string { + return PrematureCloseDialogComponent.ACTION_DESCRIPTIONS[id] ?? ''; + } + + selectClosureType(id: number): void { + this.prematureCloseForm.get('onAccountClosureId').setValue(id); + this.prematureCloseForm.get('onAccountClosureId').markAsTouched(); + } + + submit(): void { + const formValue = { ...this.prematureCloseForm.value }; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + if (formValue.closedOnDate instanceof Date) { + formValue.closedOnDate = this.dateUtils.formatDate(formValue.closedOnDate, dateFormat); + } + const data = { ...formValue, dateFormat, locale }; + this.submitting = true; + this.submitError = null; + this.fixedDepositsService + .executeFixedDepositsAccountCommand(this.data.accountId as string, 'prematureClose', data) + .subscribe({ + next: () => { + this.dialogRef.close({ confirm: true }); + }, + // Keep the dialog open with the entered values and say what went wrong, rather than leaving a + // permanently disabled Confirm behind whatever modal the global interceptor threw up. + error: (error: any) => { + this.submitting = false; + this.submitError = + error?.error?.defaultUserMessage ?? + error?.error?.errors?.[0]?.defaultUserMessage ?? + 'This account could not be prematurely closed. Please review the details and try again.'; + } + }); + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts index 42f39f6c5..a07dde825 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts @@ -17,6 +17,7 @@ import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-int import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component'; import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component'; import { PaymentDialogComponent } from './custom-dialogs/payment-dialog/payment-dialog.component'; +import { PrematureCloseDialogComponent } from './custom-dialogs/premature-close-dialog/premature-close-dialog.component'; import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component'; /** Custom Button Config. */ @@ -238,7 +239,6 @@ export class FixedDepositAccountViewComponent implements OnInit { case 'Undo Activation': case 'Add Charge': case 'Withdrawn by Client': - case 'Premature Close': case 'Deposit': case 'Withdrawal': this.router.navigate([`actions/${name}`], { relativeTo: this.route }); @@ -267,12 +267,17 @@ export class FixedDepositAccountViewComponent implements OnInit { case 'Payment': this.openPaymentDialog(); break; + case 'Premature Close': + this.openPrematureCloseDialog(); + break; } } /** - * Opens the Payment action -- unlike Premature Close/Close, works whether the account is Active or - * Matured, and is the only place a Partial Reinvest can be done. + * Opens the Payment action -- only offered once the FD has actually Matured (see + * FixedDepositsButtonsConfiguration), and the only place a Partial Reinvest can be done. Also the + * exact same dialog the FD Dashboard's Renew action opens (see fd-dashboard.component.ts's + * openRenewDialog), just without dashboardRenewal data, so its date picker stays visible. */ private openPaymentDialog(): void { const paymentDialogRef = this.dialog.open(PaymentDialogComponent, { @@ -293,6 +298,34 @@ export class FixedDepositAccountViewComponent implements OnInit { }); } + /** + * Opens the Premature Close action -- Active-only, no reinvest option, since the deposit hasn't + * matured yet. + */ + private openPrematureCloseDialog(): void { + const prematureCloseDialogRef = this.dialog.open(PrematureCloseDialogComponent, { + width: '720px', + maxWidth: '95vw', + data: { + accountId: this.fixedDepositsAccountData.id, + accountNo: this.fixedDepositsAccountData.fdAccountNumber || this.fixedDepositsAccountData.accountNo, + // calculatePrematureAmount only ever returns the final maturityAmount, not how it was reached -- + // these come from the account's own data (already loaded for this page) so the dialog can show + // the breakdown (deposit, contracted rate, penalty, interest earned) behind that one number. + depositAmount: this.fixedDepositsAccountData.depositAmount, + nominalAnnualInterestRate: this.fixedDepositsAccountData.nominalAnnualInterestRate, + preClosurePenalApplicable: this.fixedDepositsAccountData.preClosurePenalApplicable, + preClosurePenalInterest: this.fixedDepositsAccountData.preClosurePenalInterest, + preClosurePenalInterestOnType: this.fixedDepositsAccountData.preClosurePenalInterestOnType + } + }); + prematureCloseDialogRef.afterClosed().subscribe((response: any) => { + if (response?.confirm) { + this.reload(); + } + }); + } + /** * Deletes Fixed Deposits Account. */ diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html index f20e46af8..92b1d954b 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html @@ -24,9 +24,6 @@ @if (actions['Add Charge']) { } -@if (actions['Premature Close']) { - -} @if (actions['Close']) { } diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts index e0b47a2f7..b587839e8 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts @@ -15,7 +15,6 @@ import { ActivateFixedDepositsAccountComponent } from './activate-fixed-deposits import { UndoApprovalFixedDepositsAccountComponent } from './undo-approval-fixed-deposits-account/undo-approval-fixed-deposits-account.component'; import { WithdrawByClientFixedDepositsAccountComponent } from './withdraw-by-client-fixed-deposits-account/withdraw-by-client-fixed-deposits-account.component'; import { AddChargeFixedDepositsAccountComponent } from './add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component'; -import { PrematureCloseFixedDepositsAccountComponent } from './premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component'; import { CloseFixedDepositsAccountComponent } from './close-fixed-deposits-account/close-fixed-deposits-account.component'; import { FixedDepositsCashTransactionComponent } from './fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -35,7 +34,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; UndoApprovalFixedDepositsAccountComponent, WithdrawByClientFixedDepositsAccountComponent, AddChargeFixedDepositsAccountComponent, - PrematureCloseFixedDepositsAccountComponent, CloseFixedDepositsAccountComponent, FixedDepositsCashTransactionComponent ] @@ -52,7 +50,6 @@ export class FixedDepositsAccountActionsComponent { 'Undo Approval': boolean; 'Undo Activation': boolean; 'Add Charge': boolean; - 'Premature Close': boolean; 'Withdrawn by Client': boolean; Withdrawal: boolean; Deposit: boolean; @@ -64,7 +61,6 @@ export class FixedDepositsAccountActionsComponent { 'Undo Approval': false, 'Undo Activation': false, 'Add Charge': false, - 'Premature Close': false, 'Withdrawn by Client': false, Withdrawal: false, Deposit: false diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html deleted file mode 100644 index 0d204538d..000000000 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html +++ /dev/null @@ -1,105 +0,0 @@ - - -

- -
- -
- - {{ 'labels.inputs.Premature Close Date' | translate }} - - - - @if (prematureCloseAccountForm.controls.closedOnDate.hasError('required')) { - - {{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - @if (prematureCloseAccountForm.controls.maturityAmount) { - - {{ 'labels.inputs.Maturity Amount' | translate }} - - - } - - @if (prematureCloseAccountForm.contains('onAccountClosureId')) { - - {{ 'labels.inputs.Action' | translate }} - - @for (closureOption of onAccountClosureOptions; track closureOption) { - - {{ closureOption.value }} - - } - - @if (prematureCloseAccountForm.controls.onAccountClosureId.hasError('required')) { - - {{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - } - - @if (prematureCloseAccountForm.contains('toSavingsAccountId')) { - - {{ 'labels.inputs.Transfer to Savings' | translate }} - - @for (account of savingsAccountsData; track account) { - - {{ account.accountNo }} - - } - - @if (prematureCloseAccountForm.controls.toSavingsAccountId.hasError('required')) { - - {{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - } - - @if (prematureCloseAccountForm.contains('transferDescription')) { - - {{ 'labels.inputs.Transfer Description' | translate }} - - - } - - @if (prematureCloseAccountForm.contains('note')) { - - {{ 'labels.inputs.Note' | translate }} - - - } -
-
- - - - - -
-
-
diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss deleted file mode 100644 index 7c3f1555b..000000000 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss +++ /dev/null @@ -1,11 +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/. - */ - -.container { - max-width: 37rem; -} diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts deleted file mode 100644 index 5e4d5dc51..000000000 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts +++ /dev/null @@ -1,179 +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/. - */ - -/** Angular Imports */ -import { Component, OnInit, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - Validators, - UntypedFormControl, - ReactiveFormsModule -} from '@angular/forms'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; - -/** Custom Services */ -import { FixedDepositsService } from '../../fixed-deposits.service'; -import { SettingsService } from 'app/settings/settings.service'; -import { Dates } from 'app/core/utils/dates'; -import { CdkTextareaAutosize } from '@angular/cdk/text-field'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -/** - * Premature Close Fixed Deposits Account Component - */ -@Component({ - selector: 'mifosx-premature-close-fixed-deposits-account', - templateUrl: './premature-close-fixed-deposits-account.component.html', - styleUrls: ['./premature-close-fixed-deposits-account.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - CdkTextareaAutosize - ] -}) -export class PrematureCloseFixedDepositsAccountComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - private fixedDepositsService = inject(FixedDepositsService); - private dateUtils = inject(Dates); - private route = inject(ActivatedRoute); - private router = inject(Router); - private settingsService = inject(SettingsService); - - /** Minimum date allowed. */ - minDate = new Date(2000, 0, 1); - /** Maximum date allowed. */ - maxDate = new Date(); - /** Premature Close FD Account form. */ - prematureCloseAccountForm: UntypedFormGroup; - /** Savings Account Data */ - savingsAccountsData: any; - /** On account Closure Options */ - onAccountClosureOptions: any; - /** Fixed Deposits Account Id */ - accountId: any; - /** Form submission event */ - isSubmitted = false; - - /** - * @param {FormBuilder} formBuilder Form Builder - * @param {FixedDepositsService} fixedDepositsService Fixed Deposits Service - * @param {Dates} dateUtils Date Utils - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {SettingsService} settingsService Settings Service - */ - constructor() { - this.accountId = this.route.parent.snapshot.params['fixedDepositAccountId']; - } - - /** - * Creates the premature close fd account form. - */ - ngOnInit() { - this.maxDate = this.settingsService.businessDate; - this.createPrematureCloseAccountForm(); - this.buildDependencies(); - } - - /** - * Creates the premature close fd account form. - */ - createPrematureCloseAccountForm() { - this.prematureCloseAccountForm = this.formBuilder.group({ - closedOnDate: [ - '', - Validators.required - ] - }); - } - - /** - * Subscribes to value changes of parent control. - */ - buildDependencies() { - this.prematureCloseAccountForm.get('closedOnDate').valueChanges.subscribe((value: Date) => { - if (!this.isSubmitted) { - this.calculatePrematureAmount(value); - } - }); - } - - /** - * Calculates prematue amount based on closed on date. - * @param {Date} date Premature Close Date - */ - calculatePrematureAmount(date: Date) { - const locale = this.settingsService.language.code; - const dateFormat = this.settingsService.dateFormat; - const data = { - closedOnDate: this.dateUtils.formatDate(date, dateFormat), - dateFormat, - locale - }; - this.fixedDepositsService - .executeFixedDepositsAccountCommand(this.accountId, 'calculatePrematureAmount', data) - .subscribe((response: any) => { - this.savingsAccountsData = response.savingsAccounts; - this.onAccountClosureOptions = response.onAccountClosureOptions; - this.prematureCloseAccountForm.addControl( - 'maturityAmount', - new UntypedFormControl({ value: '', disabled: true }) - ); - this.prematureCloseAccountForm.addControl( - 'onAccountClosureId', - new UntypedFormControl('', Validators.required) - ); - this.prematureCloseAccountForm.addControl('note', new UntypedFormControl('')); - this.prematureCloseAccountForm.get('maturityAmount').patchValue(response.maturityAmount); - this.addTransferDetails(); - }); - } - - /** - * Subscribes to value changes of `onAccountClosureId` adds and removes transfer details accordingly. - */ - addTransferDetails() { - this.prematureCloseAccountForm.get('onAccountClosureId').valueChanges.subscribe((id: any) => { - if (id === 200) { - this.prematureCloseAccountForm.addControl( - 'toSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.prematureCloseAccountForm.addControl('transferDescription', new UntypedFormControl('')); - } else { - this.prematureCloseAccountForm.removeControl('toSavingsAccountId'); - this.prematureCloseAccountForm.removeControl('transferDescription'); - } - }); - } - - /** - * Submits the form and premature closes the fd account, - * if successful redirects to the fd account. - */ - submit() { - const prematureCloseAccountFormData = this.prematureCloseAccountForm.value; - this.isSubmitted = true; - const locale = this.settingsService.language.code; - const dateFormat = this.settingsService.dateFormat; - const prevClosedDate: Date = this.prematureCloseAccountForm.value.closedOnDate; - if (prematureCloseAccountFormData.closedOnDate instanceof Date) { - prematureCloseAccountFormData.closedOnDate = this.dateUtils.formatDate(prevClosedDate, dateFormat); - } - const data = { - ...prematureCloseAccountFormData, - dateFormat, - locale - }; - this.fixedDepositsService - .executeFixedDepositsAccountCommand(this.accountId, 'prematureClose', data) - .subscribe(() => { - this.router.navigate(['../../'], { relativeTo: this.route }); - }); - } -} diff --git a/src/app/deposits/fixed-deposits/fixed-deposits.module.ts b/src/app/deposits/fixed-deposits/fixed-deposits.module.ts index 6529b1b04..9aa9db4ba 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits.module.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits.module.ts @@ -35,7 +35,6 @@ import { ViewTransactionComponent } from './fixed-deposit-account-view/view-tran import { WaiveChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component'; import { InactivateChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component'; import { AddChargeFixedDepositsAccountComponent } from './fixed-deposits-account-actions/add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component'; -import { PrematureCloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component'; import { CloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/close-fixed-deposits-account/close-fixed-deposits-account.component'; import { CreateFixedDepositAccountComponent } from './create-fixed-deposit-account/create-fixed-deposit-account.component'; import { FixedDepositAccountDetailsStepComponent } from './fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component'; @@ -78,7 +77,6 @@ import { FixedDepositsCashTransactionComponent } from './fixed-deposits-account- WaiveChargeDialogComponent, InactivateChargeDialogComponent, AddChargeFixedDepositsAccountComponent, - PrematureCloseFixedDepositsAccountComponent, CloseFixedDepositsAccountComponent, CreateFixedDepositAccountComponent, FixedDepositAccountDetailsStepComponent, From 3f6485f6529455163c2626b096594e6054e86aba Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 28 Sep 2026 13:03:47 +0530 Subject: [PATCH 3/4] feat(fd): restrict Payment to Matured accounts, unify its behavior with Dashboard Renew Payment showed for both Active and Matured FDs, even though an Active FD already has Premature Close for closing early. Now Matured-only. That also meant a genuinely matured, past-grace FD closed via the account's own Payment button (rather than the FD Dashboard's Renew action) silently skipped crediting its gap-period interest -- the date-locking and gap-interest preview/crediting were gated behind dashboard-only data, not the account's actual matured state. Both now key off the same renewal-preview response regardless of entry point, so Payment and Renew are functionally identical once an FD has matured, as they should be since they're already the same dialog component. Co-Authored-By: Claude Sonnet 5 --- .../payment-dialog.component.html | 12 ++-- .../payment-dialog.component.ts | 67 +++++++++++-------- .../fixed-deposits-buttons.config.ts | 11 +++ 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.html index 646c9f4f9..94cd1ea54 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.html @@ -18,7 +18,7 @@ {{ isDashboardRenewalMode ? 'Renew ' + (dashboardRenewalRow?.accountNo ?? 'this matured FD') + '.' - : "Process this Fixed Deposit now -- works whether it's Active or Matured." + : 'Process this matured Fixed Deposit now.' }}

@@ -40,10 +40,10 @@ } @else {
- @if (isDashboardRenewalMode) { + @if (isMaturedRenewal) {

- {{ dashboardRenewalTimingText }} + {{ maturedRenewalTimingText }}

@if (renewalPreviewLoading) { @@ -97,7 +97,7 @@
{{ - isDashboardRenewalMode || !remainderIsApproximate + isMaturedRenewal || !remainderIsApproximate ? ('labels.inputs.Maturity Amount' | translate) : 'Amount available on the selected date' }} @@ -106,7 +106,7 @@
} - @if (!isDashboardRenewalMode) { + @if (!isMaturedRenewal) { {{ 'labels.inputs.Closed On Date' | translate }} } - @if (!isDashboardRenewalMode) { + @if (!isMaturedRenewal) { {{ 'labels.inputs.Note' | translate }} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.ts index 789d1e902..d481a87bf 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/payment-dialog/payment-dialog.component.ts @@ -133,29 +133,32 @@ export class PaymentDialogComponent implements OnInit { note: [''] }); - const dashboardRow = this.dashboardRenewalRow; - if (dashboardRow) { - // Computed from the grace window, never picked -- the date field stays in the form (the generic - // submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its picker is - // hidden in this mode (see the template), so it can't be moved off this value. - this.paymentForm - .get('closedOnDate') - .setValue(dashboardRow.withinGraceWindow ? new Date(dashboardRow.maturityDate) : this.maxDate); - this.gapInterestPending = !dashboardRow.withinGraceWindow; - - this.renewalPreviewLoading = true; - this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({ - next: (preview: any) => { - this.renewalPreview = preview; - this.renewalPreviewLoading = false; - }, - // Non-fatal -- the plain timing text above still explains what will happen even if the - // numeric breakdown couldn't be fetched; only the numbers are missing, not the action itself. - error: () => { - this.renewalPreviewLoading = false; + // Fetched every time this dialog opens, not just from the FD Dashboard -- Payment is now only ever + // offered once an FD has Matured (see FixedDepositsButtonsConfiguration), so a plain Payment on it + // is exactly the same "late renewal" scenario the Dashboard's Renew action handles, and needs the + // exact same date-locking/gap-interest treatment, or an FD closed this way instead of from the + // Dashboard would silently skip crediting interest it genuinely earned. + this.renewalPreviewLoading = true; + this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({ + next: (preview: any) => { + this.renewalPreview = preview; + this.renewalPreviewLoading = false; + if (preview?.isMatured) { + // Computed from the grace window, never picked -- the date field stays in the form (the + // generic submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its + // picker is hidden once matured (see the template), so it can't be moved off this value. + this.paymentForm + .get('closedOnDate') + .setValue(preview.withinGrace ? new Date(preview.maturityDate) : this.maxDate); + this.gapInterestPending = !preview.withinGrace; } - }); - } + }, + // Non-fatal -- Confirm still works from here (backend re-derives the same numbers on submit), + // only the numeric preview and its date-locking are unavailable, not the action itself. + error: () => { + this.renewalPreviewLoading = false; + } + }); this.paymentForm.get('onAccountClosureId').valueChanges.subscribe((id: number) => this.onClosureTypeChange(id)); // The cap depends on whether the chosen date is on or after maturity, so a date change can make an @@ -195,13 +198,19 @@ export class PaymentDialogComponent implements OnInit { return this.data.dashboardRenewal?.row ?? null; } - /** What clicking Confirm is actually about to do -- shown above the (hidden-picker) date in this - * mode, since the date itself is no longer visible to explain it. */ - get dashboardRenewalTimingText(): string { - const row = this.dashboardRenewalRow; - if (!row) return ''; - return row.withinGraceWindow - ? `Backdated to its original maturity date (${row.maturityDate}), at the currently active interest rate.` + /** True once the loaded preview confirms this FD has actually Matured -- which, since Payment is + * only ever offered on a Matured account, is every real open of this dialog now, from either entry + * point. Drives the date-locking and gap-interest preview/crediting identically either way. */ + get isMaturedRenewal(): boolean { + return !!this.renewalPreview?.isMatured; + } + + /** What clicking Confirm is actually about to do -- shown above the (hidden-picker) date once + * matured, since the date itself is no longer visible to explain it. */ + get maturedRenewalTimingText(): string { + if (!this.renewalPreview) return ''; + return this.renewalPreview.withinGrace + ? `Backdated to its original maturity date (${this.renewalPreview.maturityDate}), at the currently active interest rate.` : `This FD's grace period has passed -- gap-period interest (for the days since maturity) will be ` + `credited at the linked account's rate first, then this renews from today.`; } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts index 76abd062d..9ce40ede6 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts @@ -110,7 +110,18 @@ export class FixedDepositsButtonsConfiguration { setOptions(status: string) { switch (status) { case 'Active': + this.optionArray = [ + { + name: 'Post Interest' + }, + { + name: 'Add Charge' + } + ]; + break; case 'Matured': + // Payment only makes sense once the deposit has actually matured -- an Active FD that needs + // closing early already has Premature Close for that. this.optionArray = [ { name: 'Post Interest' From eb278978bd4b6af8cda5b6a24363f491d4d04bd1 Mon Sep 17 00:00:00 2001 From: keshav Date: Mon, 28 Sep 2026 13:36:29 +0530 Subject: [PATCH 4/4] feat: update RBI reserve return form 1 & 9 UI and FD accounting mapping --- ...ount-interest-rate-chart-step.component.ts | 22 +- .../rbi-reserve-config.component.html | 196 ++--- .../rbi-reserve-config.component.scss | 20 +- .../rbi-reserve-config.component.ts | 34 +- ...posit-product-accounting-step.component.ts | 25 +- ...eposit-product-preview-step.component.html | 1 + .../fixed-deposit-general-tab.component.html | 1 + .../rbi-circulars/rbi-circulars.service.ts | 41 ++ .../reserve-return/form1-crr.component.ts | 2 + .../reserve-return/form9-slr.component.ts | 2 + .../reserve-return/reserve-return.base.ts | 240 +++++- .../reserve-return.component.html | 682 ++++++++++++------ .../reserve-return.component.scss | 198 ++++- ...-savings-accounting-details.component.html | 8 +- ...ew-savings-accounting-details.component.ts | 2 + src/assets/translations/en-US.json | 4 + src/environments/.env.ts | 4 +- 17 files changed, 1116 insertions(+), 366 deletions(-) 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.ts 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.ts index a8ebfa8de..666f126d5 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.ts +++ 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.ts @@ -334,17 +334,18 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit); if (this.principalAmount && this.principalAmount > 0) { - // Convert tenure to months for backend API. A Days tenure under 30 days rounds down to 0 - // months, which the backend rejects -- send it as tenureInDays instead in that case. + // Years and Months convert exactly; a Days tenure is sent as days and never converted. let tenureInMonths: number | undefined; let tenureInDays: number | undefined; if (this.tenurePeriodUnit === 'Years') { tenureInMonths = this.tenureValue * 12; } else if (this.tenurePeriodUnit === 'Months') { tenureInMonths = this.tenureValue; - } else if (enteredDays >= 30) { - tenureInMonths = Math.floor(enteredDays / 30); } else { + // Sent as days, whatever the count. Converting to whole months silently dropped the + // remainder — a 200-day deposit quoted as six months lost 20 days of interest, and the + // screen still said "200 Days" above the figure. The backend takes days directly and + // works the calendar out itself. tenureInDays = enteredDays; } @@ -423,10 +424,17 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan } else { const n = 12 / compMonths; const effectiveMonths = tenureInMonths ?? enteredDays / 30; - const totalCompoundings = Math.floor(effectiveMonths / compMonths); const rPerCompounding = rate / (n * 100); - result.maturityAmount = - Math.round(this.principalAmount * Math.pow(1 + rPerCompounding, totalCompoundings) * 100) / 100; + // Whole compounding periods, then simple interest on the days left over. Flooring the + // periods and stopping there dropped the remainder entirely: a 200-day deposit + // compounding monthly quoted only six months of interest and lost the other twenty + // days, which is the depositor's money. + const wholePeriods = Math.floor(effectiveMonths / compMonths); + const compounded = this.principalAmount * Math.pow(1 + rPerCompounding, wholePeriods); + const leftoverDays = enteredDays - wholePeriods * compMonths * 30; + const leftoverInterest = + leftoverDays > 0 ? (compounded * rate * leftoverDays) / (100 * daysInYearVal) : 0; + result.maturityAmount = Math.round((compounded + leftoverInterest) * 100) / 100; } result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100; this.matchResult = { ...result }; diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html index 0e5f05cb9..9c8f007c2 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html @@ -27,101 +27,104 @@ } @else { @if (showRates) { -
-

Reserve rates

-

- Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the rate - that was actually in force then. Saving against a date that already has a rate edits it; a new date adds one. -

+
+

Reserve rates

+

+ Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the + rate that was actually in force then. Saving against a date that already has a rate edits it; a new date adds + one. +

-
- @for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) { -
- - {{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today - - @if (currentRate(type); as rate) { - {{ rate.percentage }}% - @if (rate.seedDefault) { - - Placeholder — confirm the rate actually in force - - } @else { - since {{ rate.effectiveFrom }} - } - } @else { - Not set - {{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute - } -
- } -
- -
- - Rate - - CRR — Form 1 - SLR — Form 9 - - Savings — demand share - - - - - Percentage (%) - - - - - Effective from - - - - -
- - - - - - - - - - - - - @for (rate of rates; track rate.id) { - - - - - - - + } @else { + Not set + {{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute + } + } - -
RatePercentageEffective fromStatus
{{ rate.rateType }}{{ rate.percentage }}%{{ rate.effectiveFrom }} - @if (isFuture(rate)) { - Future-dated - } @else if (currentRate(rate.rateType)?.id === rate.id) { - In force - } @else { - Superseded - } +
+ @for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) { +
+ + {{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today + + @if (currentRate(type); as rate) { + {{ rate.percentage }}% @if (rate.seedDefault) { - Placeholder + + Placeholder — confirm the rate actually in force + + } @else { + since {{ rate.effectiveFrom }} } -
- -
-
+
+ +
+ + Rate + + CRR — Form 1 + SLR — Form 9 + + Savings — demand share + + + + + Percentage (%) + + + + + Effective from + + + + +
+ + + + + + + + + + + + + @for (rate of rates; track rate.id) { + + + + + + + + } + +
RatePercentageEffective fromStatus
+ {{ rate.rateType }} + {{ rate.percentage }}%{{ rate.effectiveFrom }} + @if (isFuture(rate)) { + Future-dated + } @else if (currentRate(rate.rateType)?.id === rate.id) { + In force + } @else { + Superseded + } + @if (rate.seedDefault) { + Placeholder + } + + +
+ } @@ -324,6 +327,17 @@
+ +

+ Naming a head Memorandum - …, Annexure A - … or + Annexure B - … prints it under that section of the return instead of in the main + statement. +

+ diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss index 4b597578c..2660489ab 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.scss @@ -297,7 +297,7 @@ $red: #c5221f; &--active { border-color: $primary; - background: rgba(19, 83, 160, 0.06); + background: rgb(19 83 160 / 6%); color: $primary; } @@ -306,13 +306,13 @@ $red: #c5221f; font-weight: 500; padding: 1px 7px; border-radius: 10px; - background: rgba(176, 96, 0, 0.12); + background: rgb(176 96 0 / 12%); color: $amber; // A fully mapped return is worth stating as plainly as an incomplete one, so the tab row can // be read at a glance instead of by elimination. &--done { - background: rgba(19, 115, 51, 0.12); + background: rgb(19 115 51 / 12%); color: $green; } } @@ -379,6 +379,12 @@ $red: #c5221f; } } +// Spans the add-head form's columns rather than sitting in one of them. +.rrc-add-role__note { + flex-basis: 100%; + margin: 0 !important; +} + .rrc-remove { min-width: 0 !important; padding: 0 8px !important; @@ -397,7 +403,7 @@ $red: #c5221f; padding: 3px 10px; } -@media (max-width: 720px) { +@media (width <= 720px) { .rrc-rate-form__type, .rrc-rate-form__pct, .rrc-rate-form__date, @@ -419,7 +425,7 @@ $red: #c5221f; font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase; - color: rgba(0, 0, 0, 0.55); + color: rgb(0 0 0 / 55%); font-weight: 600; padding: 0 0.5rem 0.25rem 0; } @@ -447,7 +453,7 @@ $red: #c5221f; /* An incomplete row is marked where it is, rather than only by the disabled Save button, so it is clear which row is holding the save back. */ &__row--invalid td { - background: rgba(214, 106, 0, 0.06); + background: rgb(214 106 0 / 6%); } } @@ -460,7 +466,7 @@ $red: #c5221f; .rrc-add { margin-top: 1.25rem; padding-top: 1rem; - border-top: 1px dashed rgba(0, 0, 0, 0.12); + border-top: 1px dashed rgb(0 0 0 / 12%); &__form { display: flex; diff --git a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts index 3499691c4..73a9c7de2 100644 --- a/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts +++ b/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.ts @@ -91,12 +91,19 @@ export class RbiReserveConfigComponent implements OnInit { { value: 'TIME', label: 'Time' } ]; - /** How a head an administrator adds is consumed by the return. */ + /** + * How a head an administrator adds is consumed by the return. + * + * MAINTAINED and SLR_ASSET are separate because the two returns admit different assets: CRR is + * met in cash and balances with banks, SLR in cash, gold and approved securities. One option for + * both would let a head meant for Form 1 quietly satisfy Form 9. + */ readonly roleUsages = [ - { value: 'LIABILITY', label: 'Liability — adds to the DTL base' }, - { value: 'ASSET_NET', label: 'Asset — netted off the base (NDTL)' }, - { value: 'MAINTAINED', label: 'Counts toward what is maintained' }, - { value: 'MEMO', label: 'Memo — shown but totals nothing' } + { value: 'LIABILITY', label: 'Liability — adds to the DTL base (Form 1 & 9)' }, + { value: 'ASSET_NET', label: 'Asset — netted off the base to give NDTL (Form 1 & 9)' }, + { value: 'MAINTAINED', label: 'Counts toward CRR maintained (Form 1)' }, + { value: 'SLR_ASSET', label: 'Counts toward SLR maintained (Form 9)' }, + { value: 'MEMO', label: 'Memo — shown on the return but totals nothing' } ]; /** New-head form. */ @@ -168,10 +175,12 @@ export class RbiReserveConfigComponent implements OnInit { } byGroup.get(group)!.roles.push(role); } - this.roleGroups = [...byGroup.entries()].map(([ - name, - entry - ]) => ({ name, label: entry.label, roles: entry.roles })); + this.roleGroups = [...byGroup.entries()].map( + ([ + name, + entry + ]) => ({ name, label: entry.label, roles: entry.roles }) + ); // Keep the open tab if it still exists, so saving a role does not throw the user back to the // first tab with their place lost. @@ -237,8 +246,7 @@ export class RbiReserveConfigComponent implements OnInit { // a bank look compliant when it is not. this.selection[role.configKey] = [ ...(this.selection[role.configKey] || []), - { glAccountId: null as any, weightPercentage: 100, liabilityClass: role.classified ? 'TIME' : null } - ]; + { glAccountId: null as any, weightPercentage: 100, liabilityClass: role.classified ? 'TIME' : null }]; } removeMapping(role: any, index: number): void { @@ -252,9 +260,7 @@ export class RbiReserveConfigComponent implements OnInit { */ availableAccounts(role: any, index: number): any[] { const rows = this.selection[role.configKey] || []; - const taken = new Set( - rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`) - ); + const taken = new Set(rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`)); const current = rows[index]; return this.glAccountOptions.filter((o) => !taken.has(`${o.id}/${current?.liabilityClass ?? ''}`)); } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts index 921f9773e..0f6bb338f 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts @@ -308,6 +308,11 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha setRequired('incomeFromPenaltyAccountId', cashOrAccrual); setRequired('feesReceivableAccountId', accrualOnly); setRequired('penaltiesReceivableAccountId', accrualOnly); + // Required only on accrual, where there is no posting without it. On cash it stays optional but + // is offered, because interest posting routes through it when the product names one (see + // AccountingProcessorHelper.createInterestPostingJournalEntriesForSavings): the payable carries + // interest declared but not yet credited, and a product that names none collapses straight to + // Savings Control instead. setRequired('interestPayableAccountId', accrualOnly); setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled()); @@ -326,8 +331,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'savingsReferenceAccountId', - label: 'Savings Reference Account', - inputLabel: 'Savings reference account', + label: 'Fixed Deposit Reference Account', + inputLabel: 'Fixed deposit reference account', controlName: 'savingsReferenceAccountId', options: this.assetAccountOptions, required: true @@ -360,8 +365,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'savingsControlAccountId', - label: 'Savings Control Account', - inputLabel: 'Savings control account', + label: 'Fixed Deposit Control Account', + inputLabel: 'Fixed deposit control account', controlName: 'savingsControlAccountId', options: this.liabilityAccountOptions, required: true @@ -375,12 +380,16 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha required: true }, { + // Offered on cash accounting too, not only accrual: interest posting routes through + // this account whichever basis the product uses, so that interest declared but not yet + // credited to the member reads as the bank's own liability rather than appearing only + // when it is paid. Optional on cash - a product naming none posts straight to Savings + // Control as before. key: 'interestPayableAccountId', label: 'Interest Payable Account', inputLabel: 'Interest payable account', controlName: 'interestPayableAccountId', - options: this.liabilityAccountOptions, - accrualOnly: true + options: this.liabilityAccountOptions }, { key: 'escheatLiabilityId', @@ -402,8 +411,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha rows: [ { key: 'interestOnSavingsAccountId', - label: 'Interest On Savings Account', - inputLabel: 'Interest on savings account', + label: 'Interest On Fixed Deposits', + inputLabel: 'Interest on fixed deposits', controlName: 'interestOnSavingsAccountId', options: this.expenseAccountOptions, required: true diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html index e92b052fd..8a6431b90 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html @@ -481,6 +481,7 @@ } diff --git a/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html b/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html index cb3324ea9..458eece5b 100644 --- a/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html +++ b/src/app/products/fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-general-tab/fixed-deposit-general-tab.component.html @@ -607,6 +607,7 @@ } diff --git a/src/app/rbi-circulars/rbi-circulars.service.ts b/src/app/rbi-circulars/rbi-circulars.service.ts index 6558ba987..38fd0c260 100644 --- a/src/app/rbi-circulars/rbi-circulars.service.ts +++ b/src/app/rbi-circulars/rbi-circulars.service.ts @@ -121,6 +121,47 @@ export class RbiCircularsService { return this.http.get('/ossreturn/oss2', { params: this.ossParams(quarterEndDate, officeId) }); } + // ── Saved returns ── + + /** + * Returns saved earlier, newest first. A saved return is served from the snapshot taken when it + * was saved, never recomputed — a backdated entry or a changed mapping would otherwise produce a + * different statement from the one that was reviewed. + */ + getSavedReturns(formCode?: string): Observable { + let params = new HttpParams(); + if (formCode) { + params = params.set('formCode', formCode); + } + return this.http.get('/rbiformafiling', { params }); + } + + /** One saved return, with the stored snapshot, so the screen renders what was saved. */ + getSavedReturn(filingId: number): Observable { + return this.http.get(`/rbiformafiling/${filingId}`); + } + + /** Keeps the return currently on screen. Re-saving the same form and date replaces it. */ + saveReturn(formCode: string, reportingDate: string, report: any, remarks?: string): Observable { + return this.http.post('/rbiformafiling', { + formCode, + reportingDate, + report, + remarks, + locale: 'en', + dateFormat: 'yyyy-MM-dd' + }); + } + + /** Moves a saved return along its filing lifecycle; the snapshot itself is untouched. */ + updateSavedReturnStatus(filingId: number, status: string, remarks?: string): Observable { + return this.http.post(`/rbiformafiling/${filingId}/status`, { status, remarks }); + } + + deleteSavedReturn(filingId: number): Observable { + return this.http.delete(`/rbiformafiling/${filingId}`); + } + private ossParams(quarterEndDate?: string, officeId?: number): HttpParams { let params = new HttpParams(); if (quarterEndDate) { diff --git a/src/app/rbi-circulars/reserve-return/form1-crr.component.ts b/src/app/rbi-circulars/reserve-return/form1-crr.component.ts index 2db6c5c69..c5935ff16 100644 --- a/src/app/rbi-circulars/reserve-return/form1-crr.component.ts +++ b/src/app/rbi-circulars/reserve-return/form1-crr.component.ts @@ -30,6 +30,7 @@ import { ReserveReturnBase } from './reserve-return.base'; }) export class Form1CrrComponent extends ReserveReturnBase implements OnInit { protected readonly exportStem = 'RBI_Form1_CRR'; + protected readonly formCode = 'FORM_1'; protected fetch(reportingDate: string): Observable { return this.rbiService.getForm1(reportingDate); @@ -37,5 +38,6 @@ export class Form1CrrComponent extends ReserveReturnBase implements OnInit { ngOnInit(): void { this.load(); + this.loadSaved(); } } diff --git a/src/app/rbi-circulars/reserve-return/form9-slr.component.ts b/src/app/rbi-circulars/reserve-return/form9-slr.component.ts index 4dee07ffe..656b1fdc3 100644 --- a/src/app/rbi-circulars/reserve-return/form9-slr.component.ts +++ b/src/app/rbi-circulars/reserve-return/form9-slr.component.ts @@ -30,6 +30,7 @@ import { ReserveReturnBase } from './reserve-return.base'; }) export class Form9SlrComponent extends ReserveReturnBase implements OnInit { protected readonly exportStem = 'RBI_Form9_SLR'; + protected readonly formCode = 'FORM_9'; protected fetch(reportingDate: string): Observable { return this.rbiService.getForm9(reportingDate); @@ -37,5 +38,6 @@ export class Form9SlrComponent extends ReserveReturnBase implements OnInit { ngOnInit(): void { this.load(); + this.loadSaved(); } } diff --git a/src/app/rbi-circulars/reserve-return/reserve-return.base.ts b/src/app/rbi-circulars/reserve-return/reserve-return.base.ts index 65642760c..efa7f5962 100644 --- a/src/app/rbi-circulars/reserve-return/reserve-return.base.ts +++ b/src/app/rbi-circulars/reserve-return/reserve-return.base.ts @@ -64,7 +64,12 @@ export interface SectionRow { * and topped up on the last one fails on the daily test while passing on the closing position. */ export interface FortnightMaintenance { - days: { date: string; amount: number }[]; + /** Each day of the month, with the requirement it was measured against and the gap. */ + days: { date: string; required: number; amount: number; surplus: number }[]; + /** The month the table covers. */ + periodStart?: string; + periodEnd?: string; + periodLabel?: string; dayCount: number; average: number; lowest: number; @@ -146,6 +151,9 @@ export abstract class ReserveReturnBase { /** Filename stem for the Excel export. */ protected abstract readonly exportStem: string; + /** Which return this screen is, so saved returns list only this one's. */ + protected abstract readonly formCode: string; + data: ReserveReturn | null = null; loading = true; errorMsg = ''; @@ -158,6 +166,33 @@ export abstract class ReserveReturnBase { toastVisible = false; private toastTimer: any; + /** Returns saved earlier, listed under the live one. */ + savedReturns: any[] = []; + savedLoading = false; + saving = false; + savedRemarks = ''; + + /** + * Which half of the screen is showing: the return itself, or the ones kept on record. + * + * Two tabs rather than one long page: the live return and the saved list are read at different + * times and for different reasons, and stacking them meant scrolling past the whole statement - + * including a month of daily rows - to reach the list at all. + */ + activeTab: 'report' | 'saved' = 'report'; + + showTab(tab: 'report' | 'saved'): void { + this.activeTab = tab; + } + + /** The saved return currently open below the list, or null when none is. */ + viewingSaved: any = null; + /** + * The opened snapshot's own statement, rendered in its own section. Held separately from `data` so + * the live return stays on screen alongside it and the two can be compared directly. + */ + savedData: ReserveReturn | null = null; + load(): void { this.loading = true; this.errorMsg = ''; @@ -224,6 +259,167 @@ export abstract class ReserveReturnBase { return Math.abs(this.data?.surplusOrShortfall ?? 0); } + // ── Saved returns ── + + loadSaved(): void { + this.savedLoading = true; + this.rbiService + .getSavedReturns(this.formCode) + .pipe(catchError(() => of([]))) + .subscribe((rows: any[]) => { + this.savedLoading = false; + this.savedReturns = rows || []; + }); + } + + /** Keeps the return on screen, so it can be read again later exactly as it stands now. */ + saveCurrent(): void { + if (!this.data) { + return; + } + this.saving = true; + this.rbiService + .saveReturn(this.data.formCode, this.data.reportingDate, this.data, this.savedRemarks || undefined) + .pipe( + catchError((err: any) => { + this.saving = false; + this.toast(err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not save this return.'); + return of(null); + }) + ) + .subscribe((rows: any[] | null) => { + this.saving = false; + if (!rows) { + return; + } + this.savedReturns = rows; + this.savedRemarks = ''; + this.toast('Return saved.'); + }); + } + + /** + * Opens a saved return in its own section below the list, leaving the live return on screen. + * + * Deliberately not in place of the live one: the reason to open a saved return is almost always to + * compare it against where the bank stands now, and swapping the live figures out makes that + * comparison impossible without clicking back and forth from memory. + */ + viewSaved(row: any): void { + // Clicking the open one closes it, so the button toggles rather than re-fetching. + if (this.viewingSaved?.id === row.id) { + this.closeSaved(); + return; + } + this.rbiService + .getSavedReturn(row.id) + .pipe( + catchError(() => { + this.toast('Could not open this saved return.'); + return of(null); + }) + ) + .subscribe((saved: any) => { + if (!saved?.report) { + this.toast('This saved return has no stored statement.'); + return; + } + this.viewingSaved = saved; + this.savedData = saved.report as ReserveReturn; + // Opens on the Saved tab, where the list it was chosen from is - never jumping the user to + // the live return, which is a different statement. + this.activeTab = 'saved'; + setTimeout(() => document.getElementById('rr-saved-view')?.scrollIntoView({ behavior: 'smooth' }), 0); + }); + } + + /** Closes the opened snapshot. The live return was never hidden, so nothing is restored. */ + closeSaved(): void { + this.viewingSaved = null; + this.savedData = null; + } + + /** Kept for callers that still name the old action. */ + backToLive(): void { + this.closeSaved(); + } + + /** Magnitude of the opened snapshot's gap, so the sign lives in its label rather than the number. */ + get savedSurplus(): number { + return Math.abs(this.savedData?.surplusOrShortfall ?? 0); + } + + updateSavedStatus(row: any, status: string): void { + this.rbiService + .updateSavedReturnStatus(row.id, status) + .pipe( + catchError((err: any) => { + this.toast(err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not update the status.'); + return of(null); + }) + ) + .subscribe((rows: any[] | null) => { + if (!rows) { + return; + } + this.savedReturns = rows; + if (this.viewingSaved?.id === row.id) { + this.viewingSaved = { ...this.viewingSaved, status }; + } + this.toast(`Marked ${this.statusLabel(status)}.`); + }); + } + + deleteSaved(row: any): void { + this.rbiService + .deleteSavedReturn(row.id) + .pipe( + catchError(() => { + this.toast('Could not remove this saved return.'); + return of(null); + }) + ) + .subscribe((rows: any[] | null) => { + if (!rows) { + return; + } + this.savedReturns = rows; + if (this.viewingSaved?.id === row.id) { + this.backToLive(); + } + this.toast('Saved return removed.'); + }); + } + + /** The stages a return moves through, in order, for the status menu. */ + readonly filingStatuses = [ + 'DRAFT', + 'GENERATED', + 'CHECKED', + 'APPROVED', + 'PROVISIONAL_SUBMITTED', + 'FINAL_SUBMITTED', + 'REVISED' + ]; + + statusLabel(status: string): string { + const labels: Record = { + DRAFT: 'Draft', + GENERATED: 'Generated', + CHECKED: 'Checked', + APPROVED: 'Approved', + PROVISIONAL_SUBMITTED: 'Provisional submitted', + FINAL_SUBMITTED: 'Final submitted', + REVISED: 'Revised' + }; + return labels[status] ?? status; + } + + /** Hands the rendered return to the browser's print dialog, which also prints to PDF. */ + printReturn(): void { + window.print(); + } + /** * True when a head takes only part of an account, so the printed figure is not that account's * balance. Anything at or above 100, or unset, is the whole balance and needs no explanation. @@ -268,17 +464,25 @@ export abstract class ReserveReturnBase { try { const wb = newWorkbook(); const sheet = wb.addWorksheet(this.data.formCode.replace('_', ' ')); + // Five columns because the daily table is the widest block on the sheet; the statement above + // it uses the first three and leaves the rest empty. sheet.columns = [ { width: 52 }, { width: 22 }, - { width: 20 }]; + { width: 20 }, + { width: 22 }, + { width: 16 }]; addBanner(sheet, `${this.data.formName} — ${this.data.frequency}`, 'C'); let start = sheet.rowCount + 1; this.addKeyValue(sheet, 'Reporting date', this.data.reportingDate); if (this.data.fortnightLabel) { - this.addKeyValue(sheet, 'Fortnight', `${this.data.fortnightLabel} (${this.data.fortnightStart} to ${this.data.fortnightEnd})`); + this.addKeyValue( + sheet, + 'Fortnight', + `${this.data.fortnightLabel} (${this.data.fortnightStart} to ${this.data.fortnightEnd})` + ); } this.addKeyValue(sheet, 'DTL base as at', this.data.basis.dtlAsOnDate); this.addKeyValue(sheet, 'Balances maintained as at', this.data.basis.holdingsAsOnDate); @@ -388,16 +592,26 @@ export abstract class ReserveReturnBase { this.addKeyValue(sheet, 'Average maintained', this.amount(m.average)); this.addKeyValue(sheet, 'Required average', this.amount(m.requiredAverage)); this.addKeyValue(sheet, 'Average test', m.averageMet ? 'Met' : 'NOT MET'); - this.addKeyValue(sheet, `Daily floor (${m.dailyMinimumPercent}% of requirement)`, this.amount(m.dailyMinimumAmount)); + this.addKeyValue( + sheet, + `Daily floor (${m.dailyMinimumPercent}% of requirement)`, + this.amount(m.dailyMinimumAmount) + ); this.addKeyValue(sheet, 'Lowest day', `${this.amount(m.lowest)} on ${m.lowestOn}`); this.addKeyValue(sheet, 'Daily minimum test', m.dailyMinimumMet ? 'Met' : 'NOT MET'); - this.addKeyValue(sheet, 'Days counted', `${m.dayCount}${m.fortnightComplete ? ' (full fortnight)' : ' (to reporting date)'}`); + this.addKeyValue( + sheet, + 'Days counted', + `${m.dayCount}${m.fortnightComplete ? ' (full fortnight)' : ' (to reporting date)'}` + ); boxAllCells(sheet, start, sheet.rowCount, 2); sheet.addRow([]); const dayHead = sheet.addRow([ 'Date', - 'Maintained (INR)', + 'Required (INR)', + 'Actual (INR)', + 'Surplus / (Shortfall)', 'Against floor' ]); dayHead.font = { bold: true }; @@ -406,13 +620,21 @@ export abstract class ReserveReturnBase { for (const day of m.days) { const row = sheet.addRow([ day.date, + day.required, day.amount, + day.surplus, day.amount < m.dailyMinimumAmount ? 'Below floor' : 'OK' ]); - row.getCell(2).numFmt = '#,##0.00'; - row.getCell(2).alignment = { horizontal: 'right' }; + for (const col of [ + 2, + 3, + 4 + ]) { + row.getCell(col).numFmt = '#,##0.00'; + row.getCell(col).alignment = { horizontal: 'right' }; + } } - boxAllCells(sheet, dayStart, sheet.rowCount, 3); + boxAllCells(sheet, dayStart, sheet.rowCount, 5); } // The Memorandum and Annexures, each under its own heading. diff --git a/src/app/rbi-circulars/reserve-return/reserve-return.component.html b/src/app/rbi-circulars/reserve-return/reserve-return.component.html index 5dcc3af53..2f8dbc1a9 100644 --- a/src/app/rbi-circulars/reserve-return/reserve-return.component.html +++ b/src/app/rbi-circulars/reserve-return/reserve-return.component.html @@ -32,6 +32,14 @@
+ + + + + + @if (activeTab === 'report') { + - @if (data.unmetRequirements.length) { -
- - {{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }} - -
    - @for (reason of data.unmetRequirements; track reason) { -
  • {{ reason }}
  • - } -
- - Open RBI Reserve Configuration → - -
- } + @if (data.unmetRequirements.length) { +
+ + {{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }} + +
    + @for (reason of data.unmetRequirements; track reason) { +
  • {{ reason }}
  • + } +
+ + Open RBI Reserve Configuration → + +
+ } - -
-
-
- DTL base as at - {{ data.basis.dtlAsOnDate }} -
-
-
- Balances maintained as at - {{ data.basis.holdingsAsOnDate }} -
- @if (data.basis.rawLagDate) { -
- {{ data.basis.lagDays }}-day lag falls on - {{ data.basis.rawLagDate }} + +
+
+
+ DTL base as at + {{ data.basis.dtlAsOnDate }}
- } +
+
+ Balances maintained as at + {{ data.basis.holdingsAsOnDate }} +
+ @if (data.basis.rawLagDate) { +
+ {{ data.basis.lagDays }}-day lag falls on + {{ data.basis.rawLagDate }} +
+ } +
+

{{ data.basis.note }}

-

{{ data.basis.note }}

-
- - @if (data.computable) { -
- + @if (data.computable) { +
+ -
- NDTL base - {{ amount(data.ndtlAmount) }} - - DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at - {{ data.basis.dtlAsOnDate }} - -
-
- {{ data.rateType }} rate - {{ data.ratePercentage }}% - @if (data.rateIsSeedDefault) { - Placeholder — not confirmed - } @else { - in force on {{ data.reportingDate }} - } -
-
- Required - {{ amount(data.requiredAmount) }} - {{ data.ratePercentage }}% of NDTL -
-
- Maintained - {{ amount(data.maintainedAmount) }} - {{ percent(data.maintainedPercentage) }} of NDTL -
-
- {{ isShortfall ? 'Shortfall' : 'Surplus' }} - {{ amount(shortfallAmount) }} - {{ isShortfall ? 'Below requirement' : 'Above requirement' }} -
-
- } - - -
- - - - - - - - - - - @for (line of data.lines; track $index) { - - - - - - - @for (account of line.accounts; track account.glAccountId) { - - - - - - } - } - - @if (data.computable) { - - - - - - - - - - - - - - - - } - -
ParticularsAs atAmount (INR)
- {{ line.label }} - @if (!line.configured) { - Not configured - } - {{ line.asOnDate }} - @if (line.configured) { - {{ amount(line.amount) }} - } @else { - — - } -
- {{ account.glCode }} - {{ account.name }} - - @if (account.excluded) { - excluded — {{ account.excluded }} - } - - @if (isPartial(account)) { - - {{ account.weightPercentage }}% of {{ amount(account.accountBalance) }} - - } - - @if (account.liabilityClass) { - {{ account.liabilityClass | titlecase }} - } - {{ amount(account.balance) }}
{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL){{ amount(data.requiredAmount) }}
{{ data.rateType }} maintained{{ amount(data.maintainedAmount) }}
{{ isShortfall ? 'Shortfall' : 'Surplus' }}{{ amount(shortfallAmount) }}
-
- - - @if (data.maintenance; as m) { -
-

- Maintenance over the fortnight - @if (!m.fortnightComplete) { - In progress — {{ m.dayCount }} days so far - } -

-

- CRR is reckoned over the whole fortnight, not on one day: the full requirement must be held - on average, and at least {{ m.dailyMinimumPercent }}% of it on every single day. -

- -
-
- Average maintained - {{ amount(m.average) }} +
+ NDTL base + {{ amount(data.ndtlAmount) }} - {{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }} + DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at + {{ data.basis.dtlAsOnDate }}
-
- Lowest day - {{ amount(m.lowest) }} - - on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }} - +
+ {{ data.rateType }} rate + {{ data.ratePercentage }}% + @if (data.rateIsSeedDefault) { + Placeholder — not confirmed + } @else { + in force on {{ data.reportingDate }} + }
- Days counted - {{ m.dayCount }} - {{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }} + Required + {{ amount(data.requiredAmount) }} + {{ data.ratePercentage }}% of NDTL +
+
+ Maintained + {{ amount(data.maintainedAmount) }} + {{ percent(data.maintainedPercentage) }} of NDTL +
+
+ {{ isShortfall ? 'Shortfall' : 'Surplus' }} + {{ amount(shortfallAmount) }} + {{ isShortfall ? 'Below requirement' : 'Above requirement' }}
+ } + +
- - - + + + - @for (day of m.days; track day.date) { - - - - + + + + + + @for (account of line.accounts; track account.glAccountId) { + + + + + + } + } + + @if (data.computable) { + + + + + + + + + + + + + + }
DateMaintained (INR)Against floorParticularsAs atAmount (INR)
{{ day.date }}{{ amount(day.amount) }} - @if (day.amount < m.dailyMinimumAmount) { - Below floor - } @else { - OK + + @for (line of data.lines; track $index) { +
+ {{ line.label }} + @if (!line.configured) { + Not configured } {{ line.asOnDate }} + @if (line.configured) { + {{ amount(line.amount) }} + } @else { + — + } +
+ {{ account.glCode }} + {{ account.name }} + + @if (account.excluded) { + excluded — {{ account.excluded }} + } + + @if (isPartial(account)) { + + {{ account.weightPercentage }}% of {{ amount(account.accountBalance) }} + + } + + @if (account.liabilityClass) { + {{ account.liabilityClass | titlecase }} + } + {{ amount(account.balance) }}
{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL){{ amount(data.requiredAmount) }}
{{ data.rateType }} maintained{{ amount(data.maintainedAmount) }}
{{ isShortfall ? 'Shortfall' : 'Surplus' }}{{ amount(shortfallAmount) }}
- } - - @for (section of namedSections; track section.title) { - @if (section.rows.length) { + + @if (data.maintenance; as m) {
-

{{ section.title }}

-

{{ section.hint }}

+

+ Daily maintenance — {{ m.periodLabel || 'this month' }} + @if (!m.fortnightComplete) { + In progress — {{ m.dayCount }} days so far + } +

+

+ Every day of the month, against the requirement in force. CRR is reckoned over the period rather than on one + day: the full requirement must be held on average, and at least + {{ m.dailyMinimumPercent }}% of it on every single day. +

+ +
+
+ Average maintained + {{ amount(m.average) }} + + {{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }} + +
+
+ Lowest day + {{ amount(m.lowest) }} + on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }} +
+
+ Days counted + {{ m.dayCount }} + {{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }} +
+
+ - - + + + + + - @for (row of section.rows; track row.configKey) { - - + @for (day of m.days; track day.date) { + + + + + @@ -311,6 +305,250 @@
ParticularsAmount (INR)DateRequired (INR)Actual (INR)Surplus / (Shortfall)Against floor
- {{ row.label }} - @if (!row.configured) { - Not configured - } -
{{ day.date }}{{ amount(day.required) }}{{ amount(day.amount) }}{{ amount(day.surplus) }} - @if (row.configured) { - {{ amount(row.amount) }} + @if (day.amount < m.dailyMinimumAmount) { + Below floor } @else { - — + OK }
} + + + @for (section of namedSections; track section.title) { + @if (section.rows.length) { +
+

{{ section.title }}

+

{{ section.hint }}

+ + + + + + + + + @for (row of section.rows; track row.configKey) { + + + + + } + +
ParticularsAmount (INR)
+ {{ row.label }} + @if (!row.configured) { + Not configured + } + + @if (row.configured) { + {{ amount(row.amount) }} + } @else { + — + } +
+
+ } + } + } + } + + + @if (activeTab === 'saved') { +
+

+ Saved returns + @if (savedReturns.length) { + {{ savedReturns.length }} + } +

+

+ Returns kept for the record. Each opens exactly as it was saved — the stored statement, not a re-run of today's + ledger, so a figure already reported cannot change underneath it. +

+ + @if (data?.computable) { +
+ + Note (optional) + + + +
+ } + + @if (savedLoading) { +
Loading saved returns…
+ } @else if (!savedReturns.length) { +
Nothing saved yet. Generate a return and press Save to keep it.
+ } @else { + + + + + + + + + + + + + + + @for (row of savedReturns; track row.id) { + + + + + + + + + + + } + +
Reporting datePeriodNDTLRequiredMaintainedStatusSaved
{{ row.reportingDate }}{{ row.periodStart }} → {{ row.periodEnd }}{{ amount(row.ndtlAmount) }}{{ amount(row.requiredAmount) }}{{ amount(row.maintainedAmount) }} + + + @for (s of filingStatuses; track s) { + {{ statusLabel(s) }} + } + + + {{ row.createdOn }}
{{ row.createdBy }}
+ + +
+ } +
+ + + @if (viewingSaved && savedData; as _) { +
+
+
+

+ Saved return — {{ savedData.reportingDate }} + {{ statusLabel(viewingSaved.status) }} +

+

+ Saved {{ viewingSaved.createdOn }} by {{ viewingSaved.createdBy || 'unknown' }}. These are the figures as + they stood when it was saved — not today's ledger, which may have moved since. + @if (viewingSaved.remarks) { + “{{ viewingSaved.remarks }}” + } +

+
+ +
+ +
+
+ NDTL base + {{ amount(savedData.ndtlAmount ?? savedData.dtlAmount) }} + as at {{ savedData.basis.dtlAsOnDate }} +
+
+ {{ savedData.rateType }} rate + {{ savedData.ratePercentage }}% + as filed +
+
+ Required + {{ amount(savedData.requiredAmount) }} + {{ savedData.ratePercentage }}% of NDTL +
+
+ Maintained + {{ amount(savedData.maintainedAmount) }} + on {{ savedData.basis.holdingsAsOnDate }} +
+
+ {{ savedData.compliant ? 'Surplus' : 'Shortfall' }} + {{ amount(savedSurplus) }} + {{ savedData.compliant ? 'Above requirement' : 'Below requirement' }} +
+
+ + + + + + + + + + + @for (line of savedData.lines; track line.configKey) { + + + + + + @for (account of line.accounts; track account.glAccountId) { + + + + + + } + } + +
ParticularsAs atAmount (INR)
+ {{ line.label }} + {{ line.asOnDate }} + @if (line.configured) { + {{ amount(line.amount) }} + } @else { + — + } +
+ {{ account.glCode }} + {{ account.name }} + @if (isPartial(account)) { + + {{ account.weightPercentage }}% of {{ amount(account.accountBalance) }} + + } + @if (account.liabilityClass) { + {{ account.liabilityClass | titlecase }} + } + {{ amount(account.balance) }}
+ + @if (savedData.maintenance; as sm) { +

Daily maintenance as filed — {{ sm.periodLabel || '' }}

+ + + + + + + + + + + @for (day of sm.days; track day.date) { + + + + + + + } + +
DateRequiredActualSurplus / (Shortfall)
{{ day.date }}{{ amount(day.required) }}{{ amount(day.amount) }}{{ amount(day.surplus) }}
+ } +
} } diff --git a/src/app/rbi-circulars/reserve-return/reserve-return.component.scss b/src/app/rbi-circulars/reserve-return/reserve-return.component.scss index 6577a52db..cdc132990 100644 --- a/src/app/rbi-circulars/reserve-return/reserve-return.component.scss +++ b/src/app/rbi-circulars/reserve-return/reserve-return.component.scss @@ -129,6 +129,52 @@ $red: #c5221f; width: 180px; } +// ── Tabs ── + +.rr-tabs { + display: flex; + gap: 4px; + border-bottom: 1px solid $border; + margin-bottom: 16px; +} + +.rr-tab { + display: inline-flex; + align-items: center; + gap: 8px; + background: none; + border: none; + border-bottom: 2px solid transparent; + padding: 10px 16px; + font-size: 13px; + font-weight: 600; + color: $gray-500; + cursor: pointer; + font-family: inherit; + + i { + font-size: 12px; + } + + &:hover { + color: $gray-900; + } + + &--on { + color: $primary; + border-bottom-color: $primary; + } + + &__count { + font-size: 10.5px; + font-weight: 700; + background: #e8f0fe; + color: $primary; + border-radius: 20px; + padding: 1px 8px; + } +} + // ── Basis ── .rr-basis { @@ -297,7 +343,7 @@ $red: #c5221f; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; - padding: 10px 10px; + padding: 10px; border-bottom: 1px solid $border; white-space: nowrap; } @@ -446,6 +492,154 @@ $red: #c5221f; font-weight: 600; } +// A shortfall on a day reads as one, rather than as a number with a minus sign in front. +.rr-neg { + color: $red; + font-weight: 600; +} + +// ── Saved returns ── + +.rr-empty { + padding: 22px; + text-align: center; + color: $gray-500; + font-size: 12.5px; +} + +.rr-save-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; + + &__note { + flex: 1 1 340px; + } +} + +.rr-saved { + &__period, + &__meta { + font-size: 11px; + color: $gray-500; + white-space: nowrap; + } + + &__status { + width: 170px; + } + + &__actions { + white-space: nowrap; + } + + &__view { + font-size: 12px !important; + min-width: 0 !important; + color: $primary !important; + } + + &__del { + min-width: 0 !important; + padding: 0 8px !important; + color: $gray-500 !important; + + &:hover { + color: $red !important; + } + } + + &__remarks { + display: block; + margin-top: 4px; + color: $gray-500; + } + + &--open td { + background: #e8f0fe; + } +} + +// The opened snapshot, in its own section. Tinted and edged so it reads as a different document +// from the live return above it rather than as more of the same page. +.rr-card--saved { + border-color: #c6dafc; + background: #fbfdff; + border-left: 4px solid $primary; +} + +.rr-saved-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.rr-saved__subhead { + margin: 20px 0 8px; + font-size: 13px; + font-weight: 600; + color: $gray-900; +} + +// ── Print ── +// The browser's print dialog is also its save-as-PDF, so one stylesheet serves both. Controls, +// navigation and the saved-returns list are dropped: a printed return is a statement, and a page +// carrying dead buttons reads as a screenshot of software rather than as a filing. +@media print { + .rr-noprint, + .rr-header__actions, + .rr-unmet__link { + display: none !important; + } + + .rr-container { + max-width: none; + padding: 0; + } + + .rr-card, + .rr-basis, + .rr-stat { + border-color: #999 !important; + box-shadow: none !important; + break-inside: avoid; + } + + .rr-card { + overflow: visible !important; + } + + // Header rows repeat on every printed page, so a table spilling over a page break stays readable. + .rr-table thead { + display: table-header-group; + } + + .rr-table tr { + break-inside: avoid; + } + + .rr-toast { + display: none !important; + } + + // Backgrounds carry meaning here — a day below the floor is shaded, not merely labelled — so they + // must survive the print engine's default of dropping them to save ink. + .rr-container, + .rr-card, + .rr-basis, + .rr-stat, + .rr-table, + .rr-table th, + .rr-table td { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} + .rr-badge { display: inline-block; margin-left: 8px; @@ -479,7 +673,7 @@ $red: #c5221f; z-index: 1000; } -@media (max-width: 720px) { +@media (width <= 720px) { .rr-header__actions { width: 100%; } diff --git a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html index e121e3be3..7084e2a16 100644 --- a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html +++ b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.html @@ -21,7 +21,7 @@
@@ -72,7 +72,7 @@
@@ -81,7 +81,7 @@
@@ -143,7 +143,7 @@
diff --git a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts index 769771faf..0b3948c00 100644 --- a/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts +++ b/src/app/shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component.ts @@ -26,6 +26,8 @@ export class ViewSavingsAccountingDetailsComponent { @Input() accountingRule: OptionData; @Input() accountingMappings: any[] = []; + /** Product name for GL row titles (e.g. 'Fixed deposit'); omitted = savings wording. */ + @Input() productLabel?: string; isCashOrAccrualAccounting(): boolean { if (this.accountingRule) { diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 9160611c2..2ce8822a5 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -2236,6 +2236,9 @@ "Interest free period": "Interest free period", "Interest method": "Interest method", "Interest on savings": "Interest on savings", + "Fixed deposit control": "Fixed deposit control", + "Fixed deposit reference": "Fixed deposit reference", + "Interest on fixed deposits": "Interest on fixed deposits", "Interest rate": "Interest Rate", "Interest recalculation compounding on": "Interest recalculation compounding on", "Interest waived on": "Interest waived on", @@ -2793,6 +2796,7 @@ "Savings Product": "Savings Product", "Savings SubType": "Savings SubType", "Savings transfers in suspense": "Savings transfers in suspense", + "Transfers in suspense": "Transfers in suspense", "Schedule Date": "Schedule Date", "Share Details": "Share Details", "Scheduler Jobs": "Scheduler Jobs", diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 44f95bdbb..6a375a6e4 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260924', - 'hash': 'a6e62201' + 'version': '260928', + 'hash': '6cbb31b5' }, 'allow_switching_backend_instance': true };