Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73871a0993 | ||
|
|
7b0cb5bb63 | ||
|
|
646e73d08a | ||
|
|
a7ed3883df | ||
|
|
18c3bb6356 | ||
|
|
6afa79bd37 | ||
|
|
ec23d57734 | ||
|
|
192bc7725e | ||
|
|
eb278978bd | ||
|
|
3f6485f652 | ||
|
|
8d0d8822f0 | ||
|
|
59e1eeacc3 |
@@ -539,7 +539,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gl-table-shell">
|
<div class="gl-table-shell">
|
||||||
<mat-table [dataSource]="dataSource" matSort class="gl-mat-table">
|
<mat-table [dataSource]="dataSource" matSort matSortActive="id" matSortDirection="desc" class="gl-mat-table">
|
||||||
<!-- #ID -->
|
<!-- #ID -->
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">#</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">#</mat-header-cell>
|
||||||
|
|||||||
@@ -6041,7 +6041,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
this.editRequest = req;
|
this.editRequest = req;
|
||||||
}
|
}
|
||||||
|
|
||||||
const acTyp = 'SD';
|
// Same bug, same fix as the token-driven withdrawal-slip lookup above (see its comment): the
|
||||||
|
// request carries its own real account_type (SAVINGS, CURRENT, FIXED_DEPOSIT or
|
||||||
|
// RECURRING_DEPOSIT), and hardcoding 'SD' here searched every non-savings account on the
|
||||||
|
// wrong endpoint, so reviewing a Current-account withdrawal always failed with "Account not
|
||||||
|
// found".
|
||||||
|
const acTyp = TransactionEntryComponent.SEARCH_ACCOUNT_TYPE_TO_AC_TYP[req.account_type || ''] || 'SD';
|
||||||
const row = this.rows[0];
|
const row = this.rows[0];
|
||||||
row.acNo = req.account_no ?? '';
|
row.acNo = req.account_no ?? '';
|
||||||
row.acTyp = acTyp;
|
row.acTyp = acTyp;
|
||||||
|
|||||||
@@ -1321,6 +1321,11 @@
|
|||||||
kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched
|
kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched
|
||||||
) {
|
) {
|
||||||
<mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error>
|
<mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error>
|
||||||
|
} @else if (
|
||||||
|
kycForm.get('kycDocumentNumber')?.hasError('aadhaarLast4Mismatch') &&
|
||||||
|
kycForm.get('kycDocumentNumber')?.touched
|
||||||
|
) {
|
||||||
|
<mat-error>Last 4 digits do not match the Aadhaar number entered above</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,14 @@
|
|||||||
|
|
||||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
||||||
import { forkJoin, catchError, of } from 'rxjs';
|
import { forkJoin, catchError, of } from 'rxjs';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms';
|
import {
|
||||||
|
UntypedFormBuilder,
|
||||||
|
UntypedFormGroup,
|
||||||
|
Validators,
|
||||||
|
FormsModule,
|
||||||
|
AbstractControl,
|
||||||
|
ValidationErrors
|
||||||
|
} from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
MatDialog,
|
MatDialog,
|
||||||
MAT_DIALOG_DATA,
|
MAT_DIALOG_DATA,
|
||||||
@@ -567,6 +574,28 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
/** Existing signature document ID (for re-upload/delete) */
|
/** Existing signature document ID (for re-upload/delete) */
|
||||||
existingSignatureDocId: string = '';
|
existingSignatureDocId: string = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-field check for the OVD "Document Number" field (kycDocumentNumber): when Aadhaar Card
|
||||||
|
* is the selected KYC document, its value must identify the same Aadhaar as the aadhaarNumber
|
||||||
|
* field captured earlier in this step - the last 4 digits are what's compared, since
|
||||||
|
* aadhaarNumber may hold either the raw 12-digit number or a masked value (masking only ever
|
||||||
|
* obscures the first 8 digits - see aadhaarMasked in the kycData getter below - so the last 4
|
||||||
|
* characters are always the real digits either way). Not applicable to any other document type,
|
||||||
|
* where kycDocumentNumber is unrelated to Aadhaar entirely.
|
||||||
|
*/
|
||||||
|
private aadhaarLast4MatchValidator = (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
if (this.selectedKycDoc?.code !== 'AADHAAR') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const docNumber = (control.value || '').toString().trim();
|
||||||
|
const aadhaarValue = (control.parent?.get('aadhaarNumber')?.value || '').toString().trim();
|
||||||
|
if (docNumber.length < 4 || aadhaarValue.length < 4) {
|
||||||
|
// Required/pattern validators already flag an empty or too-short value on their own.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return docNumber.slice(-4) === aadhaarValue.slice(-4) ? null : { aadhaarLast4Mismatch: true };
|
||||||
|
};
|
||||||
|
|
||||||
/** KYC Form */
|
/** KYC Form */
|
||||||
kycForm: UntypedFormGroup = this.formBuilder.group({
|
kycForm: UntypedFormGroup = this.formBuilder.group({
|
||||||
hasPan: [true],
|
hasPan: [true],
|
||||||
@@ -616,7 +645,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
[
|
[
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/),
|
Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/),
|
||||||
Validators.maxLength(30)
|
Validators.maxLength(30),
|
||||||
|
this.aadhaarLast4MatchValidator
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
kycDocIssueDate: [''],
|
kycDocIssueDate: [''],
|
||||||
@@ -739,6 +769,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.kycForm.get('ecgcSalFlag')?.disable();
|
this.kycForm.get('ecgcSalFlag')?.disable();
|
||||||
this.kycForm.get('dormantFlag')?.disable();
|
this.kycForm.get('dormantFlag')?.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aadhaarLast4MatchValidator (on kycDocumentNumber) reads this control's value directly rather
|
||||||
|
// than through its own valueChanges, so a re-check has to be triggered explicitly whenever the
|
||||||
|
// Aadhaar field itself changes - otherwise fixing aadhaarNumber after kycDocumentNumber was
|
||||||
|
// already entered would leave a stale mismatch error in place.
|
||||||
|
this.kycForm.get('aadhaarNumber')?.valueChanges.subscribe(() => {
|
||||||
|
this.kycForm.get('kycDocumentNumber')?.updateValueAndValidity({ onlySelf: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
|
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
|
||||||
@@ -1009,6 +1047,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Reset uploads + merged preview when document type changes
|
// Reset uploads + merged preview when document type changes
|
||||||
this.kycDocFiles = { front: null, back: null };
|
this.kycDocFiles = { front: null, back: null };
|
||||||
this.kycDocMergedPreview = null;
|
this.kycDocMergedPreview = null;
|
||||||
|
// aadhaarLast4MatchValidator only applies while Aadhaar Card is selected - re-check so switching
|
||||||
|
// to/away from it immediately picks up (or clears) the mismatch error.
|
||||||
|
this.kycForm.get('kycDocumentNumber')?.updateValueAndValidity({ onlySelf: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
isKycDocSelected(proof: ProofType): boolean {
|
isKycDocSelected(proof: ProofType): boolean {
|
||||||
@@ -1189,6 +1230,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Sync chip-picker selection with the prefilled document name
|
// Sync chip-picker selection with the prefilled document name
|
||||||
if (data.kycDocumentName) {
|
if (data.kycDocumentName) {
|
||||||
this.selectedKycDoc = this.findKycDocProofByName(data.kycDocumentName);
|
this.selectedKycDoc = this.findKycDocProofByName(data.kycDocumentName);
|
||||||
|
// patchValue above ran kycDocumentNumber's validators before selectedKycDoc was known,
|
||||||
|
// so aadhaarLast4MatchValidator needs a fresh check now that it is.
|
||||||
|
this.kycForm.get('kycDocumentNumber')?.updateValueAndValidity({ onlySelf: true });
|
||||||
}
|
}
|
||||||
// Set verification statuses from existing data
|
// Set verification statuses from existing data
|
||||||
if (data.panVerified) {
|
if (data.panVerified) {
|
||||||
|
|||||||
+21
-2
@@ -54,8 +54,8 @@
|
|||||||
<mat-icon>account_balance</mat-icon>
|
<mat-icon>account_balance</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="bank-name-block">
|
<div class="bank-name-block">
|
||||||
<div class="bank-title">LincPay Co-operative Bank Ltd.</div>
|
<div class="bank-title">{{ bankName }}</div>
|
||||||
<div class="bank-subtitle">Current Account Statement</div>
|
<div class="bank-subtitle">{{ bankSubtitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="statement-label">
|
<div class="statement-label">
|
||||||
@@ -64,6 +64,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Branch line — IFSC/MICR are how a UCB customer identifies which branch issued this, and are
|
||||||
|
needed for NEFT/RTGS/cheque printing, so a real passbook always carries them. -->
|
||||||
|
@if (branchName || ifscCode || micrCode) {
|
||||||
|
<div class="branch-line">
|
||||||
|
@if (branchName) {
|
||||||
|
<span>{{ branchName }} Branch</span>
|
||||||
|
}
|
||||||
|
@if (branchAddress) {
|
||||||
|
<span>{{ branchAddress }}</span>
|
||||||
|
}
|
||||||
|
@if (ifscCode) {
|
||||||
|
<span>IFSC: {{ ifscCode }}</span>
|
||||||
|
}
|
||||||
|
@if (micrCode) {
|
||||||
|
<span>MICR: {{ micrCode }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Account Details -->
|
<!-- Account Details -->
|
||||||
<div class="account-details-band">
|
<div class="account-details-band">
|
||||||
<div class="account-details-grid">
|
<div class="account-details-grid">
|
||||||
|
|||||||
+12
@@ -127,6 +127,18 @@ $header-text: #fff;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Branch Identification Line ──────────────────────────────────────────────
|
||||||
|
.branch-line {
|
||||||
|
background: #e8edf5;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
padding: 6px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 18px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4a5a7a;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Account Details Band ────────────────────────────────────────────────────
|
// ─── Account Details Band ────────────────────────────────────────────────────
|
||||||
.account-details-band {
|
.account-details-band {
|
||||||
background: #f0f3f9;
|
background: #f0f3f9;
|
||||||
|
|||||||
+53
-1
@@ -11,11 +11,12 @@ import { Component, OnInit, OnDestroy, inject, ElementRef, ViewChild } from '@an
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { UntypedFormBuilder } from '@angular/forms';
|
import { UntypedFormBuilder } from '@angular/forms';
|
||||||
import { CurrencyPipe } from '@angular/common';
|
import { CurrencyPipe } from '@angular/common';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil, catchError, forkJoin, of } from 'rxjs';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
import { Dates } from 'app/core/utils/dates';
|
import { Dates } from 'app/core/utils/dates';
|
||||||
|
import { OrganizationService } from '../../../organization/organization.service';
|
||||||
import { Currency } from 'app/shared/models/general.model';
|
import { Currency } from 'app/shared/models/general.model';
|
||||||
import {
|
import {
|
||||||
CurrentAccountTransaction,
|
CurrentAccountTransaction,
|
||||||
@@ -44,6 +45,7 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
|
|||||||
private formBuilder = inject(UntypedFormBuilder);
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private dateUtils = inject(Dates);
|
private dateUtils = inject(Dates);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -60,6 +62,17 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
|
|||||||
availableBalance: number = 0;
|
availableBalance: number = 0;
|
||||||
activatedOnDate: any = null;
|
activatedOnDate: any = null;
|
||||||
|
|
||||||
|
/** Bank/branch letterhead — real values from Institution Profile + Branch Profile (Organization
|
||||||
|
* module) rather than a hardcoded name, so a printed passbook actually identifies which bank and
|
||||||
|
* branch issued it, the way a real UCB's core banking system does. Falls back to a generic label
|
||||||
|
* if the profile was never filled in, rather than showing a fabricated bank name. */
|
||||||
|
bankName = 'Bank';
|
||||||
|
bankSubtitle = 'Current Account Statement';
|
||||||
|
branchName = '';
|
||||||
|
branchAddress = '';
|
||||||
|
ifscCode = '';
|
||||||
|
micrCode = '';
|
||||||
|
|
||||||
/** Transactions */
|
/** Transactions */
|
||||||
allTransactions: CurrentAccountTransaction[] = [];
|
allTransactions: CurrentAccountTransaction[] = [];
|
||||||
filteredTransactions: CurrentAccountTransaction[] = [];
|
filteredTransactions: CurrentAccountTransaction[] = [];
|
||||||
@@ -92,6 +105,33 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
|
|||||||
fromDate: [''],
|
fromDate: [''],
|
||||||
toDate: ['']
|
toDate: ['']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.loadLetterhead();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Institution Profile is the bank's own single record; Branch Profile is per-office. There is no
|
||||||
|
* office id on the current account data reaching this tab, and this deployment runs a single
|
||||||
|
* branch, so the first branch profile on record is the correct one here — a genuinely
|
||||||
|
* multi-branch deployment would need the account's own office id threaded through instead. */
|
||||||
|
private loadLetterhead(): void {
|
||||||
|
forkJoin({
|
||||||
|
institution: this.organizationService.getInstitutionProfile().pipe(catchError(() => of(null))),
|
||||||
|
branches: this.organizationService.getBranchProfiles().pipe(catchError(() => of([])))
|
||||||
|
}).subscribe(({ institution, branches }: any) => {
|
||||||
|
if (institution) {
|
||||||
|
this.bankName = institution.passbookHeaderName || institution.bankName || institution.legalName || 'Bank';
|
||||||
|
this.bankSubtitle = institution.licenseNumber
|
||||||
|
? `Lic. No. ${institution.licenseNumber}`
|
||||||
|
: 'Current Account Statement';
|
||||||
|
}
|
||||||
|
const branch = Array.isArray(branches) ? branches[0] : null;
|
||||||
|
if (branch) {
|
||||||
|
this.branchName = branch.officeName || '';
|
||||||
|
this.branchAddress = branch.address || '';
|
||||||
|
this.ifscCode = branch.ifscCode || '';
|
||||||
|
this.micrCode = branch.micrCode || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeRunningBalances(transactions: CurrentAccountTransaction[]): CurrentAccountTransaction[] {
|
private computeRunningBalances(transactions: CurrentAccountTransaction[]): CurrentAccountTransaction[] {
|
||||||
@@ -279,6 +319,18 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
|
|||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Branch identification line */
|
||||||
|
.branch-line {
|
||||||
|
background: #e8edf5;
|
||||||
|
border-bottom: 1px solid #d0d7e3;
|
||||||
|
padding: 5px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 16px;
|
||||||
|
font-size: 9.5px;
|
||||||
|
color: #4a5a7a;
|
||||||
|
}
|
||||||
|
|
||||||
/* Account details band */
|
/* Account details band */
|
||||||
.account-details-band {
|
.account-details-band {
|
||||||
background: #f0f3f9;
|
background: #f0f3f9;
|
||||||
|
|||||||
+15
-7
@@ -334,17 +334,18 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
|||||||
const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit);
|
const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit);
|
||||||
|
|
||||||
if (this.principalAmount && this.principalAmount > 0) {
|
if (this.principalAmount && this.principalAmount > 0) {
|
||||||
// Convert tenure to months for backend API. A Days tenure under 30 days rounds down to 0
|
// Years and Months convert exactly; a Days tenure is sent as days and never converted.
|
||||||
// months, which the backend rejects -- send it as tenureInDays instead in that case.
|
|
||||||
let tenureInMonths: number | undefined;
|
let tenureInMonths: number | undefined;
|
||||||
let tenureInDays: number | undefined;
|
let tenureInDays: number | undefined;
|
||||||
if (this.tenurePeriodUnit === 'Years') {
|
if (this.tenurePeriodUnit === 'Years') {
|
||||||
tenureInMonths = this.tenureValue * 12;
|
tenureInMonths = this.tenureValue * 12;
|
||||||
} else if (this.tenurePeriodUnit === 'Months') {
|
} else if (this.tenurePeriodUnit === 'Months') {
|
||||||
tenureInMonths = this.tenureValue;
|
tenureInMonths = this.tenureValue;
|
||||||
} else if (enteredDays >= 30) {
|
|
||||||
tenureInMonths = Math.floor(enteredDays / 30);
|
|
||||||
} else {
|
} 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;
|
tenureInDays = enteredDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,10 +424,17 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
|||||||
} else {
|
} else {
|
||||||
const n = 12 / compMonths;
|
const n = 12 / compMonths;
|
||||||
const effectiveMonths = tenureInMonths ?? enteredDays / 30;
|
const effectiveMonths = tenureInMonths ?? enteredDays / 30;
|
||||||
const totalCompoundings = Math.floor(effectiveMonths / compMonths);
|
|
||||||
const rPerCompounding = rate / (n * 100);
|
const rPerCompounding = rate / (n * 100);
|
||||||
result.maturityAmount =
|
// Whole compounding periods, then simple interest on the days left over. Flooring the
|
||||||
Math.round(this.principalAmount * Math.pow(1 + rPerCompounding, totalCompoundings) * 100) / 100;
|
// 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;
|
result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100;
|
||||||
this.matchResult = { ...result };
|
this.matchResult = { ...result };
|
||||||
|
|||||||
+6
-6
@@ -18,7 +18,7 @@
|
|||||||
{{
|
{{
|
||||||
isDashboardRenewalMode
|
isDashboardRenewalMode
|
||||||
? 'Renew ' + (dashboardRenewalRow?.accountNo ?? 'this matured FD') + '.'
|
? 'Renew ' + (dashboardRenewalRow?.accountNo ?? 'this matured FD') + '.'
|
||||||
: "Process this Fixed Deposit now -- works whether it's Active or Matured."
|
: 'Process this matured Fixed Deposit now.'
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<form [formGroup]="paymentForm">
|
<form [formGroup]="paymentForm">
|
||||||
@if (isDashboardRenewalMode) {
|
@if (isMaturedRenewal) {
|
||||||
<p class="pmt__hint">
|
<p class="pmt__hint">
|
||||||
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
|
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
|
||||||
{{ dashboardRenewalTimingText }}
|
{{ maturedRenewalTimingText }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (renewalPreviewLoading) {
|
@if (renewalPreviewLoading) {
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
<div class="pmt__stat">
|
<div class="pmt__stat">
|
||||||
<span class="pmt__stat-label">
|
<span class="pmt__stat-label">
|
||||||
{{
|
{{
|
||||||
isDashboardRenewalMode || !remainderIsApproximate
|
isMaturedRenewal || !remainderIsApproximate
|
||||||
? ('labels.inputs.Maturity Amount' | translate)
|
? ('labels.inputs.Maturity Amount' | translate)
|
||||||
: 'Amount available on the selected date'
|
: 'Amount available on the selected date'
|
||||||
}}
|
}}
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!isDashboardRenewalMode) {
|
@if (!isMaturedRenewal) {
|
||||||
<mat-form-field appearance="outline" class="pmt__date" (click)="closedOnDatePicker.open()">
|
<mat-form-field appearance="outline" class="pmt__date" (click)="closedOnDatePicker.open()">
|
||||||
<mat-label>{{ 'labels.inputs.Closed On Date' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Closed On Date' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!isDashboardRenewalMode) {
|
@if (!isMaturedRenewal) {
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
||||||
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||||||
|
|||||||
+38
-29
@@ -133,29 +133,32 @@ export class PaymentDialogComponent implements OnInit {
|
|||||||
note: ['']
|
note: ['']
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboardRow = this.dashboardRenewalRow;
|
// Fetched every time this dialog opens, not just from the FD Dashboard -- Payment is now only ever
|
||||||
if (dashboardRow) {
|
// offered once an FD has Matured (see FixedDepositsButtonsConfiguration), so a plain Payment on it
|
||||||
// Computed from the grace window, never picked -- the date field stays in the form (the generic
|
// is exactly the same "late renewal" scenario the Dashboard's Renew action handles, and needs the
|
||||||
// submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its picker is
|
// exact same date-locking/gap-interest treatment, or an FD closed this way instead of from the
|
||||||
// hidden in this mode (see the template), so it can't be moved off this value.
|
// Dashboard would silently skip crediting interest it genuinely earned.
|
||||||
this.paymentForm
|
this.renewalPreviewLoading = true;
|
||||||
.get('closedOnDate')
|
this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({
|
||||||
.setValue(dashboardRow.withinGraceWindow ? new Date(dashboardRow.maturityDate) : this.maxDate);
|
next: (preview: any) => {
|
||||||
this.gapInterestPending = !dashboardRow.withinGraceWindow;
|
this.renewalPreview = preview;
|
||||||
|
this.renewalPreviewLoading = false;
|
||||||
this.renewalPreviewLoading = true;
|
if (preview?.isMatured) {
|
||||||
this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({
|
// Computed from the grace window, never picked -- the date field stays in the form (the
|
||||||
next: (preview: any) => {
|
// generic submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its
|
||||||
this.renewalPreview = preview;
|
// picker is hidden once matured (see the template), so it can't be moved off this value.
|
||||||
this.renewalPreviewLoading = false;
|
this.paymentForm
|
||||||
},
|
.get('closedOnDate')
|
||||||
// Non-fatal -- the plain timing text above still explains what will happen even if the
|
.setValue(preview.withinGrace ? new Date(preview.maturityDate) : this.maxDate);
|
||||||
// numeric breakdown couldn't be fetched; only the numbers are missing, not the action itself.
|
this.gapInterestPending = !preview.withinGrace;
|
||||||
error: () => {
|
|
||||||
this.renewalPreviewLoading = false;
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
// 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));
|
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
|
// 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;
|
return this.data.dashboardRenewal?.row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What clicking Confirm is actually about to do -- shown above the (hidden-picker) date in this
|
/** True once the loaded preview confirms this FD has actually Matured -- which, since Payment is
|
||||||
* mode, since the date itself is no longer visible to explain it. */
|
* only ever offered on a Matured account, is every real open of this dialog now, from either entry
|
||||||
get dashboardRenewalTimingText(): string {
|
* point. Drives the date-locking and gap-interest preview/crediting identically either way. */
|
||||||
const row = this.dashboardRenewalRow;
|
get isMaturedRenewal(): boolean {
|
||||||
if (!row) return '';
|
return !!this.renewalPreview?.isMatured;
|
||||||
return row.withinGraceWindow
|
}
|
||||||
? `Backdated to its original maturity date (${row.maturityDate}), at the currently active interest rate.`
|
|
||||||
|
/** 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 ` +
|
: `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.`;
|
`credited at the linked account's rate first, then this renews from today.`;
|
||||||
}
|
}
|
||||||
|
|||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pcd">
|
||||||
|
<div class="pcd__header">
|
||||||
|
<div class="pcd__header-left">
|
||||||
|
<span class="pcd__header-ico"><fa-icon icon="times-circle"></fa-icon></span>
|
||||||
|
<div class="pcd__header-text">
|
||||||
|
<h2 class="pcd__title">{{ 'labels.heading.Premature Close' | translate }}</h2>
|
||||||
|
<p class="pcd__subtitle">
|
||||||
|
Close {{ data.accountNo ?? 'this account' }} before maturity -- interest is recalculated as of the date below,
|
||||||
|
usually at a lower, penalty rate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pcd__header-close" mat-dialog-close [disabled]="submitting" title="Close">
|
||||||
|
<fa-icon icon="times"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mat-dialog-content class="pcd__content">
|
||||||
|
@if (loadFailed) {
|
||||||
|
<div class="pcd__error" role="alert">
|
||||||
|
<fa-icon icon="times-circle" class="m-r-10"></fa-icon>
|
||||||
|
Could not calculate the premature-closure amount for this account. Close this dialog and try again.
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<form [formGroup]="prematureCloseForm">
|
||||||
|
<mat-form-field appearance="outline" class="pcd__date" (click)="closedOnDatePicker.open()">
|
||||||
|
<mat-label>{{ 'labels.inputs.Premature Close Date' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[min]="minDate"
|
||||||
|
[max]="maxDate"
|
||||||
|
[matDatepicker]="closedOnDatePicker"
|
||||||
|
required
|
||||||
|
formControlName="closedOnDate"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="closedOnDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #closedOnDatePicker></mat-datepicker>
|
||||||
|
@if (prematureCloseForm.controls.closedOnDate.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
{{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (loadingAmount) {
|
||||||
|
<div class="pcd__loading">
|
||||||
|
<mat-spinner diameter="28"></mat-spinner>
|
||||||
|
<span>Calculating premature-closure amount...</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
@if (maturityAmount !== null && maturityAmount !== undefined) {
|
||||||
|
<div class="pcd__preview">
|
||||||
|
@if (data.depositAmount) {
|
||||||
|
<div class="pcd__preview-row">
|
||||||
|
<span class="pcd__preview-label">Deposit Amount</span>
|
||||||
|
<span class="pcd__preview-value">{{ data.depositAmount | formatNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (data.nominalAnnualInterestRate) {
|
||||||
|
<div class="pcd__preview-row">
|
||||||
|
<span class="pcd__preview-label">Contracted Interest Rate</span>
|
||||||
|
<span class="pcd__preview-value">{{ data.nominalAnnualInterestRate }}%</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (data.preClosurePenalApplicable) {
|
||||||
|
<div class="pcd__preview-row">
|
||||||
|
<span class="pcd__preview-label"
|
||||||
|
>Pre-Closure Penalty (on {{ data.preClosurePenalInterestOnType?.value }})</span
|
||||||
|
>
|
||||||
|
<span class="pcd__preview-value">− {{ data.preClosurePenalInterest }}%</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (interestEarned !== null) {
|
||||||
|
<div class="pcd__preview-row pcd__preview-row--highlight">
|
||||||
|
<span class="pcd__preview-label"
|
||||||
|
>Interest Earned (to {{ 'labels.inputs.Premature Close Date' | translate }})</span
|
||||||
|
>
|
||||||
|
<span class="pcd__preview-value">+ {{ interestEarned | formatNumber }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="pcd__preview-row pcd__preview-row--total">
|
||||||
|
<span class="pcd__preview-label">{{ 'labels.inputs.Maturity Amount' | translate }}</span>
|
||||||
|
<span class="pcd__preview-value">{{ maturityAmount | formatNumber }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="pcd__section-label" id="pcd-action-label">{{ 'labels.inputs.Action' | translate }}</div>
|
||||||
|
<div class="pcd__options" role="radiogroup" aria-labelledby="pcd-action-label">
|
||||||
|
@for (closureOption of onAccountClosureOptions; track closureOption) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pcd__option"
|
||||||
|
role="radio"
|
||||||
|
[attr.aria-checked]="prematureCloseForm.get('onAccountClosureId').value === closureOption.id"
|
||||||
|
[class.pcd__option--active]="prematureCloseForm.get('onAccountClosureId').value === closureOption.id"
|
||||||
|
(click)="selectClosureType(closureOption.id)"
|
||||||
|
>
|
||||||
|
<span class="pcd__option-ico"><fa-icon [icon]="iconFor(closureOption.id)"></fa-icon></span>
|
||||||
|
<span class="pcd__option-text">
|
||||||
|
<span class="pcd__option-title">{{ closureOption.value }}</span>
|
||||||
|
<span class="pcd__option-desc">{{ descriptionFor(closureOption.id) }}</span>
|
||||||
|
</span>
|
||||||
|
@if (prematureCloseForm.get('onAccountClosureId').value === closureOption.id) {
|
||||||
|
<fa-icon icon="check-circle" class="pcd__option-check"></fa-icon>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (
|
||||||
|
prematureCloseForm.controls.onAccountClosureId?.hasError('required') &&
|
||||||
|
prematureCloseForm.controls.onAccountClosureId?.touched
|
||||||
|
) {
|
||||||
|
<div class="pcd__option-error">
|
||||||
|
{{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
{{ 'labels.commons.required' | translate }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (noDestinationAccounts) {
|
||||||
|
<p class="pcd__hint pcd__hint--warn" role="alert">
|
||||||
|
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
|
||||||
|
This customer has no active savings account to transfer into. Open one first, or choose an action that
|
||||||
|
pays out in cash instead.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (prematureCloseForm.contains('toSavingsAccountId')) {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Transfer to Savings' | translate }}</mat-label>
|
||||||
|
<mat-select required formControlName="toSavingsAccountId">
|
||||||
|
@for (account of savingsAccountsData; track account) {
|
||||||
|
<mat-option [value]="account.id">
|
||||||
|
{{ account.accountNo }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (prematureCloseForm.controls.toSavingsAccountId?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
{{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (prematureCloseForm.contains('transferDescription')) {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label>
|
||||||
|
<input matInput formControlName="transferDescription" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (prematureCloseForm.contains('note')) {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
||||||
|
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (submitError) {
|
||||||
|
<p class="pcd__error" role="alert">
|
||||||
|
<fa-icon icon="times-circle" class="m-r-5"></fa-icon>
|
||||||
|
{{ submitError }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mat-dialog-actions align="end" class="pcd__actions">
|
||||||
|
<button mat-button mat-dialog-close [disabled]="submitting" class="pcd__btn-cancel">
|
||||||
|
{{ 'labels.buttons.Cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
class="pcd__btn-submit"
|
||||||
|
[disabled]="!prematureCloseForm?.valid || submitting || loadFailed || loadingAmount"
|
||||||
|
(click)="submit()"
|
||||||
|
>
|
||||||
|
{{ submitting ? ('labels.buttons.Processing' | translate) : ('labels.buttons.Confirm' | translate) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+388
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+218
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<number, string> = {
|
||||||
|
[PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'money-bill-wave',
|
||||||
|
[PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'wallet'
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly ACTION_DESCRIPTIONS: Record<number, string> = {
|
||||||
|
[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<PrematureCloseDialogComponent>>(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.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
-3
@@ -17,6 +17,7 @@ import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-int
|
|||||||
import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component';
|
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 { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component';
|
||||||
import { PaymentDialogComponent } from './custom-dialogs/payment-dialog/payment-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';
|
import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component';
|
||||||
|
|
||||||
/** Custom Button Config. */
|
/** Custom Button Config. */
|
||||||
@@ -238,7 +239,6 @@ export class FixedDepositAccountViewComponent implements OnInit {
|
|||||||
case 'Undo Activation':
|
case 'Undo Activation':
|
||||||
case 'Add Charge':
|
case 'Add Charge':
|
||||||
case 'Withdrawn by Client':
|
case 'Withdrawn by Client':
|
||||||
case 'Premature Close':
|
|
||||||
case 'Deposit':
|
case 'Deposit':
|
||||||
case 'Withdrawal':
|
case 'Withdrawal':
|
||||||
this.router.navigate([`actions/${name}`], { relativeTo: this.route });
|
this.router.navigate([`actions/${name}`], { relativeTo: this.route });
|
||||||
@@ -267,12 +267,17 @@ export class FixedDepositAccountViewComponent implements OnInit {
|
|||||||
case 'Payment':
|
case 'Payment':
|
||||||
this.openPaymentDialog();
|
this.openPaymentDialog();
|
||||||
break;
|
break;
|
||||||
|
case 'Premature Close':
|
||||||
|
this.openPrematureCloseDialog();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the Payment action -- unlike Premature Close/Close, works whether the account is Active or
|
* Opens the Payment action -- only offered once the FD has actually Matured (see
|
||||||
* Matured, and is the only place a Partial Reinvest can be done.
|
* 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 {
|
private openPaymentDialog(): void {
|
||||||
const paymentDialogRef = this.dialog.open(PaymentDialogComponent, {
|
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.
|
* Deletes Fixed Deposits Account.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+11
@@ -110,7 +110,18 @@ export class FixedDepositsButtonsConfiguration {
|
|||||||
setOptions(status: string) {
|
setOptions(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Active':
|
case 'Active':
|
||||||
|
this.optionArray = [
|
||||||
|
{
|
||||||
|
name: 'Post Interest'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Add Charge'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
break;
|
||||||
case 'Matured':
|
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 = [
|
this.optionArray = [
|
||||||
{
|
{
|
||||||
name: 'Post Interest'
|
name: 'Post Interest'
|
||||||
|
|||||||
-3
@@ -24,9 +24,6 @@
|
|||||||
@if (actions['Add Charge']) {
|
@if (actions['Add Charge']) {
|
||||||
<mifosx-add-charge-fixed-deposits-account></mifosx-add-charge-fixed-deposits-account>
|
<mifosx-add-charge-fixed-deposits-account></mifosx-add-charge-fixed-deposits-account>
|
||||||
}
|
}
|
||||||
@if (actions['Premature Close']) {
|
|
||||||
<mifosx-premature-close-fixed-deposits-account></mifosx-premature-close-fixed-deposits-account>
|
|
||||||
}
|
|
||||||
@if (actions['Close']) {
|
@if (actions['Close']) {
|
||||||
<mifosx-close-fixed-deposits-account></mifosx-close-fixed-deposits-account>
|
<mifosx-close-fixed-deposits-account></mifosx-close-fixed-deposits-account>
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -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 { 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 { 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 { 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 { CloseFixedDepositsAccountComponent } from './close-fixed-deposits-account/close-fixed-deposits-account.component';
|
||||||
import { FixedDepositsCashTransactionComponent } from './fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component';
|
import { FixedDepositsCashTransactionComponent } from './fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
@@ -35,7 +34,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
UndoApprovalFixedDepositsAccountComponent,
|
UndoApprovalFixedDepositsAccountComponent,
|
||||||
WithdrawByClientFixedDepositsAccountComponent,
|
WithdrawByClientFixedDepositsAccountComponent,
|
||||||
AddChargeFixedDepositsAccountComponent,
|
AddChargeFixedDepositsAccountComponent,
|
||||||
PrematureCloseFixedDepositsAccountComponent,
|
|
||||||
CloseFixedDepositsAccountComponent,
|
CloseFixedDepositsAccountComponent,
|
||||||
FixedDepositsCashTransactionComponent
|
FixedDepositsCashTransactionComponent
|
||||||
]
|
]
|
||||||
@@ -52,7 +50,6 @@ export class FixedDepositsAccountActionsComponent {
|
|||||||
'Undo Approval': boolean;
|
'Undo Approval': boolean;
|
||||||
'Undo Activation': boolean;
|
'Undo Activation': boolean;
|
||||||
'Add Charge': boolean;
|
'Add Charge': boolean;
|
||||||
'Premature Close': boolean;
|
|
||||||
'Withdrawn by Client': boolean;
|
'Withdrawn by Client': boolean;
|
||||||
Withdrawal: boolean;
|
Withdrawal: boolean;
|
||||||
Deposit: boolean;
|
Deposit: boolean;
|
||||||
@@ -64,7 +61,6 @@ export class FixedDepositsAccountActionsComponent {
|
|||||||
'Undo Approval': false,
|
'Undo Approval': false,
|
||||||
'Undo Activation': false,
|
'Undo Activation': false,
|
||||||
'Add Charge': false,
|
'Add Charge': false,
|
||||||
'Premature Close': false,
|
|
||||||
'Withdrawn by Client': false,
|
'Withdrawn by Client': false,
|
||||||
Withdrawal: false,
|
Withdrawal: false,
|
||||||
Deposit: false
|
Deposit: false
|
||||||
|
|||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright since 2025 Mifos Initiative
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<mat-card>
|
|
||||||
<form [formGroup]="prematureCloseAccountForm" (ngSubmit)="submit()">
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="layout-column">
|
|
||||||
<mat-form-field (click)="closedOnDatePicker.open()">
|
|
||||||
<mat-label>{{ 'labels.inputs.Premature Close Date' | translate }}</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
[min]="minDate"
|
|
||||||
[max]="maxDate"
|
|
||||||
[matDatepicker]="closedOnDatePicker"
|
|
||||||
required
|
|
||||||
formControlName="closedOnDate"
|
|
||||||
/>
|
|
||||||
<mat-datepicker-toggle matSuffix [for]="closedOnDatePicker"></mat-datepicker-toggle>
|
|
||||||
<mat-datepicker #closedOnDatePicker></mat-datepicker>
|
|
||||||
@if (prematureCloseAccountForm.controls.closedOnDate.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.controls.maturityAmount) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Maturity Amount' | translate }}</mat-label>
|
|
||||||
<input type="number" matInput formControlName="maturityAmount" />
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('onAccountClosureId')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Action' | translate }}</mat-label>
|
|
||||||
<mat-select required formControlName="onAccountClosureId">
|
|
||||||
@for (closureOption of onAccountClosureOptions; track closureOption) {
|
|
||||||
<mat-option [value]="closureOption.id">
|
|
||||||
{{ closureOption.value }}
|
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
@if (prematureCloseAccountForm.controls.onAccountClosureId.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('toSavingsAccountId')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Transfer to Savings' | translate }}</mat-label>
|
|
||||||
<mat-select required formControlName="toSavingsAccountId">
|
|
||||||
@for (account of savingsAccountsData; track account) {
|
|
||||||
<mat-option [value]="account.id">
|
|
||||||
{{ account.accountNo }}
|
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
@if (prematureCloseAccountForm.controls.toSavingsAccountId.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('transferDescription')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="transferDescription" />
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (prematureCloseAccountForm.contains('note')) {
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
|
||||||
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
|
|
||||||
<button type="button" mat-raised-button [routerLink]="['../../']">
|
|
||||||
{{ 'labels.buttons.Cancel' | translate }}
|
|
||||||
</button>
|
|
||||||
<button mat-raised-button color="warn" [disabled]="!prematureCloseAccountForm.valid">
|
|
||||||
{{ 'labels.buttons.Confirm' | translate }}
|
|
||||||
</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
</form>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
-11
@@ -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;
|
|
||||||
}
|
|
||||||
-179
@@ -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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 { 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 { 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 { 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 { 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 { 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';
|
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,
|
WaiveChargeDialogComponent,
|
||||||
InactivateChargeDialogComponent,
|
InactivateChargeDialogComponent,
|
||||||
AddChargeFixedDepositsAccountComponent,
|
AddChargeFixedDepositsAccountComponent,
|
||||||
PrematureCloseFixedDepositsAccountComponent,
|
|
||||||
CloseFixedDepositsAccountComponent,
|
CloseFixedDepositsAccountComponent,
|
||||||
CreateFixedDepositAccountComponent,
|
CreateFixedDepositAccountComponent,
|
||||||
FixedDepositAccountDetailsStepComponent,
|
FixedDepositAccountDetailsStepComponent,
|
||||||
|
|||||||
@@ -27,101 +27,104 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<!-- ═══ Rates — only the reserve returns are charged at a percentage ═══ -->
|
<!-- ═══ Rates — only the reserve returns are charged at a percentage ═══ -->
|
||||||
@if (showRates) {
|
@if (showRates) {
|
||||||
<div class="rrc-card">
|
<div class="rrc-card">
|
||||||
<h3 class="rrc-card__title">Reserve rates</h3>
|
<h3 class="rrc-card__title">Reserve rates</h3>
|
||||||
<p class="rrc-card__hint">
|
<p class="rrc-card__hint">
|
||||||
Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the rate
|
Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the
|
||||||
that was actually in force then. Saving against a date that already has a rate edits it; a new date adds one.
|
rate that was actually in force then. Saving against a date that already has a rate edits it; a new date adds
|
||||||
</p>
|
one.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="rrc-rate-now">
|
<div class="rrc-rate-now">
|
||||||
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
|
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
|
||||||
<div class="rrc-rate-now__item">
|
<div class="rrc-rate-now__item">
|
||||||
<span class="rrc-rate-now__label">
|
<span class="rrc-rate-now__label">
|
||||||
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
|
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
|
||||||
</span>
|
</span>
|
||||||
@if (currentRate(type); as rate) {
|
@if (currentRate(type); as rate) {
|
||||||
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
|
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
|
||||||
@if (rate.seedDefault) {
|
|
||||||
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
|
|
||||||
Placeholder — confirm the rate actually in force
|
|
||||||
</span>
|
|
||||||
} @else {
|
|
||||||
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
|
|
||||||
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rrc-rate-form">
|
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
|
|
||||||
<mat-label>Rate</mat-label>
|
|
||||||
<mat-select [(ngModel)]="newRate.rateType">
|
|
||||||
<mat-option value="CRR">CRR — Form 1</mat-option>
|
|
||||||
<mat-option value="SLR">SLR — Form 9</mat-option>
|
|
||||||
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
|
|
||||||
savings balances that counts as a demand liability, the rest being time. -->
|
|
||||||
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
|
|
||||||
<mat-label>Percentage (%)</mat-label>
|
|
||||||
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
|
|
||||||
<mat-label>Effective from</mat-label>
|
|
||||||
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
|
|
||||||
{{ savingRate ? 'Saving…' : 'Save rate' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="rrc-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Rate</th>
|
|
||||||
<th>Percentage</th>
|
|
||||||
<th>Effective from</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (rate of rates; track rate.id) {
|
|
||||||
<tr>
|
|
||||||
<td><span class="rrc-key">{{ rate.rateType }}</span></td>
|
|
||||||
<td class="rrc-num">{{ rate.percentage }}%</td>
|
|
||||||
<td>{{ rate.effectiveFrom }}</td>
|
|
||||||
<td>
|
|
||||||
@if (isFuture(rate)) {
|
|
||||||
<span class="rrc-badge rrc-badge--future">Future-dated</span>
|
|
||||||
} @else if (currentRate(rate.rateType)?.id === rate.id) {
|
|
||||||
<span class="rrc-badge rrc-badge--active">In force</span>
|
|
||||||
} @else {
|
|
||||||
<span class="rrc-badge rrc-badge--past">Superseded</span>
|
|
||||||
}
|
|
||||||
@if (rate.seedDefault) {
|
@if (rate.seedDefault) {
|
||||||
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
|
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
|
||||||
|
Placeholder — confirm the rate actually in force
|
||||||
|
</span>
|
||||||
|
} @else {
|
||||||
|
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
|
||||||
}
|
}
|
||||||
</td>
|
} @else {
|
||||||
<td>
|
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
|
||||||
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
|
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
|
||||||
<i class="fas fa-trash"></i>
|
}
|
||||||
</button>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
</div>
|
<div class="rrc-rate-form">
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
|
||||||
|
<mat-label>Rate</mat-label>
|
||||||
|
<mat-select [(ngModel)]="newRate.rateType">
|
||||||
|
<mat-option value="CRR">CRR — Form 1</mat-option>
|
||||||
|
<mat-option value="SLR">SLR — Form 9</mat-option>
|
||||||
|
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
|
||||||
|
savings balances that counts as a demand liability, the rest being time. -->
|
||||||
|
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
|
||||||
|
<mat-label>Percentage (%)</mat-label>
|
||||||
|
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
|
||||||
|
<mat-label>Effective from</mat-label>
|
||||||
|
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
|
||||||
|
{{ savingRate ? 'Saving…' : 'Save rate' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="rrc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rate</th>
|
||||||
|
<th>Percentage</th>
|
||||||
|
<th>Effective from</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (rate of rates; track rate.id) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="rrc-key">{{ rate.rateType }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="rrc-num">{{ rate.percentage }}%</td>
|
||||||
|
<td>{{ rate.effectiveFrom }}</td>
|
||||||
|
<td>
|
||||||
|
@if (isFuture(rate)) {
|
||||||
|
<span class="rrc-badge rrc-badge--future">Future-dated</span>
|
||||||
|
} @else if (currentRate(rate.rateType)?.id === rate.id) {
|
||||||
|
<span class="rrc-badge rrc-badge--active">In force</span>
|
||||||
|
} @else {
|
||||||
|
<span class="rrc-badge rrc-badge--past">Superseded</span>
|
||||||
|
}
|
||||||
|
@if (rate.seedDefault) {
|
||||||
|
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- ═══ GL accounts ═══ -->
|
<!-- ═══ GL accounts ═══ -->
|
||||||
@@ -324,6 +327,17 @@
|
|||||||
<input matInput [(ngModel)]="newRole.description" placeholder="What this head covers" />
|
<input matInput [(ngModel)]="newRole.description" placeholder="What this head covers" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The section prefixes are a naming convention rather than a field, so they have to be
|
||||||
|
stated somewhere: an administrator who wants a head on the Memorandum has no way to
|
||||||
|
discover that naming it so is what puts it there.
|
||||||
|
-->
|
||||||
|
<p class="rrc-card__hint rrc-add-role__note">
|
||||||
|
Naming a head <strong>Memorandum - …</strong>, <strong>Annexure A - …</strong> or
|
||||||
|
<strong>Annexure B - …</strong> prints it under that section of the return instead of in the main
|
||||||
|
statement.
|
||||||
|
</p>
|
||||||
|
|
||||||
<button mat-raised-button color="primary" [disabled]="addingRole" (click)="addRole()">
|
<button mat-raised-button color="primary" [disabled]="addingRole" (click)="addRole()">
|
||||||
{{ addingRole ? 'Adding…' : 'Add head' }}
|
{{ addingRole ? 'Adding…' : 'Add head' }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ $red: #c5221f;
|
|||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
background: rgba(19, 83, 160, 0.06);
|
background: rgb(19 83 160 / 6%);
|
||||||
color: $primary;
|
color: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,13 +306,13 @@ $red: #c5221f;
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 1px 7px;
|
padding: 1px 7px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(176, 96, 0, 0.12);
|
background: rgb(176 96 0 / 12%);
|
||||||
color: $amber;
|
color: $amber;
|
||||||
|
|
||||||
// A fully mapped return is worth stating as plainly as an incomplete one, so the tab row can
|
// 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.
|
// be read at a glance instead of by elimination.
|
||||||
&--done {
|
&--done {
|
||||||
background: rgba(19, 115, 51, 0.12);
|
background: rgb(19 115 51 / 12%);
|
||||||
color: $green;
|
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 {
|
.rrc-remove {
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
padding: 0 8px !important;
|
padding: 0 8px !important;
|
||||||
@@ -397,7 +403,7 @@ $red: #c5221f;
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (width <= 720px) {
|
||||||
.rrc-rate-form__type,
|
.rrc-rate-form__type,
|
||||||
.rrc-rate-form__pct,
|
.rrc-rate-form__pct,
|
||||||
.rrc-rate-form__date,
|
.rrc-rate-form__date,
|
||||||
@@ -419,7 +425,7 @@ $red: #c5221f;
|
|||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: rgba(0, 0, 0, 0.55);
|
color: rgb(0 0 0 / 55%);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0 0.5rem 0.25rem 0;
|
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
|
/* 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. */
|
clear which row is holding the save back. */
|
||||||
&__row--invalid td {
|
&__row--invalid td {
|
||||||
background: rgba(214, 106, 0, 0.06);
|
background: rgb(214 106 0 / 6%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,7 +466,7 @@ $red: #c5221f;
|
|||||||
.rrc-add {
|
.rrc-add {
|
||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
border-top: 1px dashed rgba(0, 0, 0, 0.12);
|
border-top: 1px dashed rgb(0 0 0 / 12%);
|
||||||
|
|
||||||
&__form {
|
&__form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -91,12 +91,19 @@ export class RbiReserveConfigComponent implements OnInit {
|
|||||||
{ value: 'TIME', label: 'Time' }
|
{ 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 = [
|
readonly roleUsages = [
|
||||||
{ value: 'LIABILITY', label: 'Liability — adds to the DTL base' },
|
{ value: 'LIABILITY', label: 'Liability — adds to the DTL base (Form 1 & 9)' },
|
||||||
{ value: 'ASSET_NET', label: 'Asset — netted off the base (NDTL)' },
|
{ value: 'ASSET_NET', label: 'Asset — netted off the base to give NDTL (Form 1 & 9)' },
|
||||||
{ value: 'MAINTAINED', label: 'Counts toward what is maintained' },
|
{ value: 'MAINTAINED', label: 'Counts toward CRR maintained (Form 1)' },
|
||||||
{ value: 'MEMO', label: 'Memo — shown but totals nothing' }
|
{ value: 'SLR_ASSET', label: 'Counts toward SLR maintained (Form 9)' },
|
||||||
|
{ value: 'MEMO', label: 'Memo — shown on the return but totals nothing' }
|
||||||
];
|
];
|
||||||
|
|
||||||
/** New-head form. */
|
/** New-head form. */
|
||||||
@@ -168,10 +175,12 @@ export class RbiReserveConfigComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
byGroup.get(group)!.roles.push(role);
|
byGroup.get(group)!.roles.push(role);
|
||||||
}
|
}
|
||||||
this.roleGroups = [...byGroup.entries()].map(([
|
this.roleGroups = [...byGroup.entries()].map(
|
||||||
name,
|
([
|
||||||
entry
|
name,
|
||||||
]) => ({ name, label: entry.label, roles: entry.roles }));
|
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
|
// 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.
|
// first tab with their place lost.
|
||||||
@@ -237,8 +246,7 @@ export class RbiReserveConfigComponent implements OnInit {
|
|||||||
// a bank look compliant when it is not.
|
// a bank look compliant when it is not.
|
||||||
this.selection[role.configKey] = [
|
this.selection[role.configKey] = [
|
||||||
...(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 {
|
removeMapping(role: any, index: number): void {
|
||||||
@@ -252,9 +260,7 @@ export class RbiReserveConfigComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
availableAccounts(role: any, index: number): any[] {
|
availableAccounts(role: any, index: number): any[] {
|
||||||
const rows = this.selection[role.configKey] || [];
|
const rows = this.selection[role.configKey] || [];
|
||||||
const taken = new Set(
|
const taken = new Set(rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`));
|
||||||
rows.filter((_, i) => i !== index).map((m) => `${m.glAccountId}/${m.liabilityClass ?? ''}`)
|
|
||||||
);
|
|
||||||
const current = rows[index];
|
const current = rows[index];
|
||||||
return this.glAccountOptions.filter((o) => !taken.has(`${o.id}/${current?.liabilityClass ?? ''}`));
|
return this.glAccountOptions.filter((o) => !taken.has(`${o.id}/${current?.liabilityClass ?? ''}`));
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -308,6 +308,11 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
|
|||||||
setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
|
setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
|
||||||
setRequired('feesReceivableAccountId', accrualOnly);
|
setRequired('feesReceivableAccountId', accrualOnly);
|
||||||
setRequired('penaltiesReceivableAccountId', 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('interestPayableAccountId', accrualOnly);
|
||||||
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
|
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
|
||||||
|
|
||||||
@@ -326,8 +331,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
|
|||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
key: 'savingsReferenceAccountId',
|
key: 'savingsReferenceAccountId',
|
||||||
label: 'Savings Reference Account',
|
label: 'Fixed Deposit Reference Account',
|
||||||
inputLabel: 'Savings reference account',
|
inputLabel: 'Fixed deposit reference account',
|
||||||
controlName: 'savingsReferenceAccountId',
|
controlName: 'savingsReferenceAccountId',
|
||||||
options: this.assetAccountOptions,
|
options: this.assetAccountOptions,
|
||||||
required: true
|
required: true
|
||||||
@@ -360,8 +365,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
|
|||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
key: 'savingsControlAccountId',
|
key: 'savingsControlAccountId',
|
||||||
label: 'Savings Control Account',
|
label: 'Fixed Deposit Control Account',
|
||||||
inputLabel: 'Savings control account',
|
inputLabel: 'Fixed deposit control account',
|
||||||
controlName: 'savingsControlAccountId',
|
controlName: 'savingsControlAccountId',
|
||||||
options: this.liabilityAccountOptions,
|
options: this.liabilityAccountOptions,
|
||||||
required: true
|
required: true
|
||||||
@@ -375,12 +380,16 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
|
|||||||
required: true
|
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',
|
key: 'interestPayableAccountId',
|
||||||
label: 'Interest Payable Account',
|
label: 'Interest Payable Account',
|
||||||
inputLabel: 'Interest payable account',
|
inputLabel: 'Interest payable account',
|
||||||
controlName: 'interestPayableAccountId',
|
controlName: 'interestPayableAccountId',
|
||||||
options: this.liabilityAccountOptions,
|
options: this.liabilityAccountOptions
|
||||||
accrualOnly: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'escheatLiabilityId',
|
key: 'escheatLiabilityId',
|
||||||
@@ -402,8 +411,8 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
|
|||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
key: 'interestOnSavingsAccountId',
|
key: 'interestOnSavingsAccountId',
|
||||||
label: 'Interest On Savings Account',
|
label: 'Interest On Fixed Deposits',
|
||||||
inputLabel: 'Interest on savings account',
|
inputLabel: 'Interest on fixed deposits',
|
||||||
controlName: 'interestOnSavingsAccountId',
|
controlName: 'interestOnSavingsAccountId',
|
||||||
options: this.expenseAccountOptions,
|
options: this.expenseAccountOptions,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
+1
@@ -481,6 +481,7 @@
|
|||||||
<mifosx-view-savings-accounting-details
|
<mifosx-view-savings-accounting-details
|
||||||
[accountingRule]="accountingRule"
|
[accountingRule]="accountingRule"
|
||||||
[accountingMappings]="accountingMappings"
|
[accountingMappings]="accountingMappings"
|
||||||
|
productLabel="Fixed deposit"
|
||||||
></mifosx-view-savings-accounting-details>
|
></mifosx-view-savings-accounting-details>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -607,6 +607,7 @@
|
|||||||
<mifosx-view-savings-accounting-details
|
<mifosx-view-savings-accounting-details
|
||||||
[accountingRule]="fixedDepositProductData.accountingRule"
|
[accountingRule]="fixedDepositProductData.accountingRule"
|
||||||
[accountingMappings]="fixedDepositProductData.accountingMappings"
|
[accountingMappings]="fixedDepositProductData.accountingMappings"
|
||||||
|
productLabel="Fixed deposit"
|
||||||
></mifosx-view-savings-accounting-details>
|
></mifosx-view-savings-accounting-details>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -121,6 +121,47 @@ export class RbiCircularsService {
|
|||||||
return this.http.get('/ossreturn/oss2', { params: this.ossParams(quarterEndDate, officeId) });
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
return this.http.post(`/rbiformafiling/${filingId}/status`, { status, remarks });
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSavedReturn(filingId: number): Observable<any> {
|
||||||
|
return this.http.delete(`/rbiformafiling/${filingId}`);
|
||||||
|
}
|
||||||
|
|
||||||
private ossParams(quarterEndDate?: string, officeId?: number): HttpParams {
|
private ossParams(quarterEndDate?: string, officeId?: number): HttpParams {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (quarterEndDate) {
|
if (quarterEndDate) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { ReserveReturnBase } from './reserve-return.base';
|
|||||||
})
|
})
|
||||||
export class Form1CrrComponent extends ReserveReturnBase implements OnInit {
|
export class Form1CrrComponent extends ReserveReturnBase implements OnInit {
|
||||||
protected readonly exportStem = 'RBI_Form1_CRR';
|
protected readonly exportStem = 'RBI_Form1_CRR';
|
||||||
|
protected readonly formCode = 'FORM_1';
|
||||||
|
|
||||||
protected fetch(reportingDate: string): Observable<any> {
|
protected fetch(reportingDate: string): Observable<any> {
|
||||||
return this.rbiService.getForm1(reportingDate);
|
return this.rbiService.getForm1(reportingDate);
|
||||||
@@ -37,5 +38,6 @@ export class Form1CrrComponent extends ReserveReturnBase implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.load();
|
this.load();
|
||||||
|
this.loadSaved();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { ReserveReturnBase } from './reserve-return.base';
|
|||||||
})
|
})
|
||||||
export class Form9SlrComponent extends ReserveReturnBase implements OnInit {
|
export class Form9SlrComponent extends ReserveReturnBase implements OnInit {
|
||||||
protected readonly exportStem = 'RBI_Form9_SLR';
|
protected readonly exportStem = 'RBI_Form9_SLR';
|
||||||
|
protected readonly formCode = 'FORM_9';
|
||||||
|
|
||||||
protected fetch(reportingDate: string): Observable<any> {
|
protected fetch(reportingDate: string): Observable<any> {
|
||||||
return this.rbiService.getForm9(reportingDate);
|
return this.rbiService.getForm9(reportingDate);
|
||||||
@@ -37,5 +38,6 @@ export class Form9SlrComponent extends ReserveReturnBase implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.load();
|
this.load();
|
||||||
|
this.loadSaved();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
* and topped up on the last one fails on the daily test while passing on the closing position.
|
||||||
*/
|
*/
|
||||||
export interface FortnightMaintenance {
|
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;
|
dayCount: number;
|
||||||
average: number;
|
average: number;
|
||||||
lowest: number;
|
lowest: number;
|
||||||
@@ -146,6 +151,9 @@ export abstract class ReserveReturnBase {
|
|||||||
/** Filename stem for the Excel export. */
|
/** Filename stem for the Excel export. */
|
||||||
protected abstract readonly exportStem: string;
|
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;
|
data: ReserveReturn | null = null;
|
||||||
loading = true;
|
loading = true;
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
@@ -158,6 +166,33 @@ export abstract class ReserveReturnBase {
|
|||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: any;
|
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 {
|
load(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.errorMsg = '';
|
this.errorMsg = '';
|
||||||
@@ -224,6 +259,167 @@ export abstract class ReserveReturnBase {
|
|||||||
return Math.abs(this.data?.surplusOrShortfall ?? 0);
|
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<string, string> = {
|
||||||
|
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
|
* 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.
|
* 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 {
|
try {
|
||||||
const wb = newWorkbook();
|
const wb = newWorkbook();
|
||||||
const sheet = wb.addWorksheet(this.data.formCode.replace('_', ' '));
|
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 = [
|
sheet.columns = [
|
||||||
{ width: 52 },
|
{ width: 52 },
|
||||||
{ width: 22 },
|
{ width: 22 },
|
||||||
{ width: 20 }];
|
{ width: 20 },
|
||||||
|
{ width: 22 },
|
||||||
|
{ width: 16 }];
|
||||||
|
|
||||||
addBanner(sheet, `${this.data.formName} — ${this.data.frequency}`, 'C');
|
addBanner(sheet, `${this.data.formName} — ${this.data.frequency}`, 'C');
|
||||||
|
|
||||||
let start = sheet.rowCount + 1;
|
let start = sheet.rowCount + 1;
|
||||||
this.addKeyValue(sheet, 'Reporting date', this.data.reportingDate);
|
this.addKeyValue(sheet, 'Reporting date', this.data.reportingDate);
|
||||||
if (this.data.fortnightLabel) {
|
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, 'DTL base as at', this.data.basis.dtlAsOnDate);
|
||||||
this.addKeyValue(sheet, 'Balances maintained as at', this.data.basis.holdingsAsOnDate);
|
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, 'Average maintained', this.amount(m.average));
|
||||||
this.addKeyValue(sheet, 'Required average', this.amount(m.requiredAverage));
|
this.addKeyValue(sheet, 'Required average', this.amount(m.requiredAverage));
|
||||||
this.addKeyValue(sheet, 'Average test', m.averageMet ? 'Met' : 'NOT MET');
|
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, 'Lowest day', `${this.amount(m.lowest)} on ${m.lowestOn}`);
|
||||||
this.addKeyValue(sheet, 'Daily minimum test', m.dailyMinimumMet ? 'Met' : 'NOT MET');
|
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);
|
boxAllCells(sheet, start, sheet.rowCount, 2);
|
||||||
|
|
||||||
sheet.addRow([]);
|
sheet.addRow([]);
|
||||||
const dayHead = sheet.addRow([
|
const dayHead = sheet.addRow([
|
||||||
'Date',
|
'Date',
|
||||||
'Maintained (INR)',
|
'Required (INR)',
|
||||||
|
'Actual (INR)',
|
||||||
|
'Surplus / (Shortfall)',
|
||||||
'Against floor'
|
'Against floor'
|
||||||
]);
|
]);
|
||||||
dayHead.font = { bold: true };
|
dayHead.font = { bold: true };
|
||||||
@@ -406,13 +620,21 @@ export abstract class ReserveReturnBase {
|
|||||||
for (const day of m.days) {
|
for (const day of m.days) {
|
||||||
const row = sheet.addRow([
|
const row = sheet.addRow([
|
||||||
day.date,
|
day.date,
|
||||||
|
day.required,
|
||||||
day.amount,
|
day.amount,
|
||||||
|
day.surplus,
|
||||||
day.amount < m.dailyMinimumAmount ? 'Below floor' : 'OK'
|
day.amount < m.dailyMinimumAmount ? 'Below floor' : 'OK'
|
||||||
]);
|
]);
|
||||||
row.getCell(2).numFmt = '#,##0.00';
|
for (const col of [
|
||||||
row.getCell(2).alignment = { horizontal: 'right' };
|
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.
|
// The Memorandum and Annexures, each under its own heading.
|
||||||
|
|||||||
@@ -32,6 +32,14 @@
|
|||||||
<input matInput type="date" [(ngModel)]="reportingDate" (change)="onDateChange()" />
|
<input matInput type="date" [(ngModel)]="reportingDate" (change)="onDateChange()" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-stroked-button (click)="resetToToday()">Today</button>
|
<button mat-stroked-button (click)="resetToToday()">Today</button>
|
||||||
|
<button mat-stroked-button (click)="printReturn()">
|
||||||
|
<i class="fas fa-print"></i>
|
||||||
|
Print
|
||||||
|
</button>
|
||||||
|
<button mat-stroked-button [disabled]="saving || !data.computable" (click)="saveCurrent()">
|
||||||
|
<i class="fas fa-bookmark"></i>
|
||||||
|
{{ saving ? 'Saving…' : 'Save' }}
|
||||||
|
</button>
|
||||||
<button mat-raised-button color="primary" [disabled]="generating" (click)="exportWorkbook()">
|
<button mat-raised-button color="primary" [disabled]="generating" (click)="exportWorkbook()">
|
||||||
<i class="fas fa-file-excel"></i>
|
<i class="fas fa-file-excel"></i>
|
||||||
{{ generating ? 'Generating…' : 'Export' }}
|
{{ generating ? 'Generating…' : 'Export' }}
|
||||||
@@ -40,269 +48,255 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
═══ Tabs ═══
|
||||||
|
The return and the saved list are read at different times and for different reasons, so they
|
||||||
|
are two halves of the screen rather than one page where reaching the list means scrolling past
|
||||||
|
a whole statement and a month of daily rows.
|
||||||
|
-->
|
||||||
|
<div class="rr-tabs rr-noprint">
|
||||||
|
<button class="rr-tab" [class.rr-tab--on]="activeTab === 'report'" (click)="showTab('report')">
|
||||||
|
<i class="fas fa-file-invoice"></i> Report
|
||||||
|
</button>
|
||||||
|
<button class="rr-tab" [class.rr-tab--on]="activeTab === 'saved'" (click)="showTab('saved')">
|
||||||
|
<i class="fas fa-bookmark"></i> Saved returns
|
||||||
|
@if (savedReturns.length) {
|
||||||
|
<span class="rr-tab__count">{{ savedReturns.length }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (activeTab === 'report') {
|
||||||
|
<!--
|
||||||
Caveats show whenever there are any — not only when the return cannot compute. A return
|
Caveats show whenever there are any — not only when the return cannot compute. A return
|
||||||
computed on the placeholder rate is arithmetically right and still must not be filed as-is,
|
computed on the placeholder rate is arithmetically right and still must not be filed as-is,
|
||||||
so hiding that behind the not-computable branch would bury the one thing worth saying.
|
so hiding that behind the not-computable branch would bury the one thing worth saying.
|
||||||
-->
|
-->
|
||||||
@if (data.unmetRequirements.length) {
|
@if (data.unmetRequirements.length) {
|
||||||
<div class="rr-alert rr-alert--warn">
|
<div class="rr-alert rr-alert--warn">
|
||||||
<strong>
|
<strong>
|
||||||
{{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }}
|
{{ data.computable ? 'Check this before filing.' : 'This return cannot be computed yet.' }}
|
||||||
</strong>
|
</strong>
|
||||||
<ul class="rr-unmet">
|
<ul class="rr-unmet">
|
||||||
@for (reason of data.unmetRequirements; track reason) {
|
@for (reason of data.unmetRequirements; track reason) {
|
||||||
<li>{{ reason }}</li>
|
<li>{{ reason }}</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<a class="rr-unmet__link" [routerLink]="['/organization/rbi-reserve-config']">
|
<a class="rr-unmet__link" [routerLink]="['/organization/rbi-reserve-config']">
|
||||||
Open RBI Reserve Configuration →
|
Open RBI Reserve Configuration →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- ═══ Basis ═══ -->
|
<!-- ═══ Basis ═══ -->
|
||||||
<div class="rr-basis">
|
<div class="rr-basis">
|
||||||
<div class="rr-basis__row">
|
<div class="rr-basis__row">
|
||||||
<div class="rr-basis__item">
|
<div class="rr-basis__item">
|
||||||
<span class="rr-basis__label">DTL base as at</span>
|
<span class="rr-basis__label">DTL base as at</span>
|
||||||
<span class="rr-basis__value">{{ data.basis.dtlAsOnDate }}</span>
|
<span class="rr-basis__value">{{ data.basis.dtlAsOnDate }}</span>
|
||||||
</div>
|
|
||||||
<div class="rr-basis__arrow"><i class="fas fa-arrow-right"></i></div>
|
|
||||||
<div class="rr-basis__item">
|
|
||||||
<span class="rr-basis__label">Balances maintained as at</span>
|
|
||||||
<span class="rr-basis__value">{{ data.basis.holdingsAsOnDate }}</span>
|
|
||||||
</div>
|
|
||||||
@if (data.basis.rawLagDate) {
|
|
||||||
<div class="rr-basis__item rr-basis__item--muted">
|
|
||||||
<span class="rr-basis__label">{{ data.basis.lagDays }}-day lag falls on</span>
|
|
||||||
<span class="rr-basis__value">{{ data.basis.rawLagDate }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div class="rr-basis__arrow"><i class="fas fa-arrow-right"></i></div>
|
||||||
|
<div class="rr-basis__item">
|
||||||
|
<span class="rr-basis__label">Balances maintained as at</span>
|
||||||
|
<span class="rr-basis__value">{{ data.basis.holdingsAsOnDate }}</span>
|
||||||
|
</div>
|
||||||
|
@if (data.basis.rawLagDate) {
|
||||||
|
<div class="rr-basis__item rr-basis__item--muted">
|
||||||
|
<span class="rr-basis__label">{{ data.basis.lagDays }}-day lag falls on</span>
|
||||||
|
<span class="rr-basis__value">{{ data.basis.rawLagDate }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p class="rr-basis__note">{{ data.basis.note }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="rr-basis__note">{{ data.basis.note }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ═══ Summary ═══ -->
|
<!-- ═══ Summary ═══ -->
|
||||||
@if (data.computable) {
|
@if (data.computable) {
|
||||||
<div class="rr-summary">
|
<div class="rr-summary">
|
||||||
<!-- NDTL, not DTL: the rate is charged on the net figure, so the base shown must be the one
|
<!-- NDTL, not DTL: the rate is charged on the net figure, so the base shown must be the one
|
||||||
the required amount was actually computed from. DTL and what was netted off it are
|
the required amount was actually computed from. DTL and what was netted off it are
|
||||||
given underneath, so the subtraction is visible rather than implied. -->
|
given underneath, so the subtraction is visible rather than implied. -->
|
||||||
<div class="rr-stat">
|
<div class="rr-stat">
|
||||||
<span class="rr-stat__label">NDTL base</span>
|
<span class="rr-stat__label">NDTL base</span>
|
||||||
<span class="rr-stat__value">{{ amount(data.ndtlAmount) }}</span>
|
<span class="rr-stat__value">{{ amount(data.ndtlAmount) }}</span>
|
||||||
<span class="rr-stat__sub">
|
|
||||||
DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at
|
|
||||||
{{ data.basis.dtlAsOnDate }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="rr-stat" [class.rr-stat--provisional]="data.rateIsSeedDefault">
|
|
||||||
<span class="rr-stat__label">{{ data.rateType }} rate</span>
|
|
||||||
<span class="rr-stat__value">{{ data.ratePercentage }}%</span>
|
|
||||||
@if (data.rateIsSeedDefault) {
|
|
||||||
<span class="rr-stat__sub rr-stat__sub--warn">Placeholder — not confirmed</span>
|
|
||||||
} @else {
|
|
||||||
<span class="rr-stat__sub">in force on {{ data.reportingDate }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="rr-stat">
|
|
||||||
<span class="rr-stat__label">Required</span>
|
|
||||||
<span class="rr-stat__value">{{ amount(data.requiredAmount) }}</span>
|
|
||||||
<span class="rr-stat__sub">{{ data.ratePercentage }}% of NDTL</span>
|
|
||||||
</div>
|
|
||||||
<div class="rr-stat">
|
|
||||||
<span class="rr-stat__label">Maintained</span>
|
|
||||||
<span class="rr-stat__value">{{ amount(data.maintainedAmount) }}</span>
|
|
||||||
<span class="rr-stat__sub">{{ percent(data.maintainedPercentage) }} of NDTL</span>
|
|
||||||
</div>
|
|
||||||
<div class="rr-stat" [class.rr-stat--bad]="isShortfall" [class.rr-stat--good]="!isShortfall">
|
|
||||||
<span class="rr-stat__label">{{ isShortfall ? 'Shortfall' : 'Surplus' }}</span>
|
|
||||||
<span class="rr-stat__value">{{ amount(shortfallAmount) }}</span>
|
|
||||||
<span class="rr-stat__sub">{{ isShortfall ? 'Below requirement' : 'Above requirement' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- ═══ Lines ═══ -->
|
|
||||||
<div class="rr-card">
|
|
||||||
<table class="rr-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Particulars</th>
|
|
||||||
<th>As at</th>
|
|
||||||
<th class="rr-table__num">Amount (INR)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<!-- Tracked by position, not by configKey: derived rows (the demand/time split, NDTL)
|
|
||||||
have no config key of their own, so keying on it would collide them into one. -->
|
|
||||||
@for (line of data.lines; track $index) {
|
|
||||||
<tr
|
|
||||||
class="rr-line"
|
|
||||||
[class.rr-line--unconfigured]="!line.configured"
|
|
||||||
[class.rr-line--derived]="line.derived"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<span class="rr-line__label">{{ line.label }}</span>
|
|
||||||
@if (!line.configured) {
|
|
||||||
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td class="rr-line__date">{{ line.asOnDate }}</td>
|
|
||||||
<td class="rr-table__num">
|
|
||||||
@if (line.configured) {
|
|
||||||
{{ amount(line.amount) }}
|
|
||||||
} @else {
|
|
||||||
<span class="rr-dash">—</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Account-level breakdown, so every total traces back to the accounts behind it. -->
|
|
||||||
@for (account of line.accounts; track account.glAccountId) {
|
|
||||||
<tr class="rr-subline" [class.rr-subline--excluded]="account.excluded">
|
|
||||||
<td>
|
|
||||||
<span class="rr-subline__code">{{ account.glCode }}</span>
|
|
||||||
{{ account.name }}
|
|
||||||
<!-- An account still pinned to the role but out of the total: named here so the
|
|
||||||
line's figure can be read as short by it, rather than looking complete. -->
|
|
||||||
@if (account.excluded) {
|
|
||||||
<span class="rr-subline__excluded">excluded — {{ account.excluded }}</span>
|
|
||||||
}
|
|
||||||
<!-- Where only part of an account is taken, the figure below is not the account's
|
|
||||||
balance and must not be read as one: state the whole balance and the share of
|
|
||||||
it this head takes, so the two can be reconciled without opening the ledger. -->
|
|
||||||
@if (isPartial(account)) {
|
|
||||||
<span class="rr-subline__portion">
|
|
||||||
{{ account.weightPercentage }}% of {{ amount(account.accountBalance) }}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
<!-- Which head the portion falls under, on a liability line that carries both. -->
|
|
||||||
@if (account.liabilityClass) {
|
|
||||||
<span class="rr-subline__class">{{ account.liabilityClass | titlecase }}</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
<td class="rr-table__num">{{ amount(account.balance) }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (data.computable) {
|
|
||||||
<tr class="rr-total">
|
|
||||||
<td>{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL)</td>
|
|
||||||
<td></td>
|
|
||||||
<td class="rr-table__num">{{ amount(data.requiredAmount) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="rr-total">
|
|
||||||
<td>{{ data.rateType }} maintained</td>
|
|
||||||
<td></td>
|
|
||||||
<td class="rr-table__num">{{ amount(data.maintainedAmount) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="rr-total" [class.rr-total--bad]="isShortfall">
|
|
||||||
<td>{{ isShortfall ? 'Shortfall' : 'Surplus' }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td class="rr-table__num">{{ amount(shortfallAmount) }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
═══ Fortnight maintenance ═══
|
|
||||||
CRR is not met by the closing day's balance: the requirement must be held on average across
|
|
||||||
the fortnight AND a daily minimum on every day of it. A bank can pass the closing position and
|
|
||||||
still fail, so both tests are stated.
|
|
||||||
-->
|
|
||||||
@if (data.maintenance; as m) {
|
|
||||||
<div class="rr-card rr-card--section">
|
|
||||||
<h3 class="rr-section__title">
|
|
||||||
Maintenance over the fortnight
|
|
||||||
@if (!m.fortnightComplete) {
|
|
||||||
<span class="rr-badge rr-badge--partial">In progress — {{ m.dayCount }} days so far</span>
|
|
||||||
}
|
|
||||||
</h3>
|
|
||||||
<p class="rr-section__hint">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="rr-summary">
|
|
||||||
<div class="rr-stat" [class.rr-stat--good]="m.averageMet" [class.rr-stat--bad]="!m.averageMet">
|
|
||||||
<span class="rr-stat__label">Average maintained</span>
|
|
||||||
<span class="rr-stat__value">{{ amount(m.average) }}</span>
|
|
||||||
<span class="rr-stat__sub">
|
<span class="rr-stat__sub">
|
||||||
{{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }}
|
DTL {{ amount(data.dtlAmount) }} less {{ amount(data.interBankAssets) }} · as at
|
||||||
|
{{ data.basis.dtlAsOnDate }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-stat" [class.rr-stat--good]="m.dailyMinimumMet" [class.rr-stat--bad]="!m.dailyMinimumMet">
|
<div class="rr-stat" [class.rr-stat--provisional]="data.rateIsSeedDefault">
|
||||||
<span class="rr-stat__label">Lowest day</span>
|
<span class="rr-stat__label">{{ data.rateType }} rate</span>
|
||||||
<span class="rr-stat__value">{{ amount(m.lowest) }}</span>
|
<span class="rr-stat__value">{{ data.ratePercentage }}%</span>
|
||||||
<span class="rr-stat__sub">
|
@if (data.rateIsSeedDefault) {
|
||||||
on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }}
|
<span class="rr-stat__sub rr-stat__sub--warn">Placeholder — not confirmed</span>
|
||||||
</span>
|
} @else {
|
||||||
|
<span class="rr-stat__sub">in force on {{ data.reportingDate }}</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-stat">
|
<div class="rr-stat">
|
||||||
<span class="rr-stat__label">Days counted</span>
|
<span class="rr-stat__label">Required</span>
|
||||||
<span class="rr-stat__value">{{ m.dayCount }}</span>
|
<span class="rr-stat__value">{{ amount(data.requiredAmount) }}</span>
|
||||||
<span class="rr-stat__sub">{{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }}</span>
|
<span class="rr-stat__sub">{{ data.ratePercentage }}% of NDTL</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat">
|
||||||
|
<span class="rr-stat__label">Maintained</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(data.maintainedAmount) }}</span>
|
||||||
|
<span class="rr-stat__sub">{{ percent(data.maintainedPercentage) }} of NDTL</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat" [class.rr-stat--bad]="isShortfall" [class.rr-stat--good]="!isShortfall">
|
||||||
|
<span class="rr-stat__label">{{ isShortfall ? 'Shortfall' : 'Surplus' }}</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(shortfallAmount) }}</span>
|
||||||
|
<span class="rr-stat__sub">{{ isShortfall ? 'Below requirement' : 'Above requirement' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- ═══ Lines ═══ -->
|
||||||
|
<div class="rr-card">
|
||||||
<table class="rr-table">
|
<table class="rr-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Particulars</th>
|
||||||
<th class="rr-table__num">Maintained (INR)</th>
|
<th>As at</th>
|
||||||
<th class="rr-table__num">Against floor</th>
|
<th class="rr-table__num">Amount (INR)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (day of m.days; track day.date) {
|
<!-- Tracked by position, not by configKey: derived rows (the demand/time split, NDTL)
|
||||||
<tr [class.rr-day--short]="day.amount < m.dailyMinimumAmount">
|
have no config key of their own, so keying on it would collide them into one. -->
|
||||||
<td>{{ day.date }}</td>
|
@for (line of data.lines; track $index) {
|
||||||
<td class="rr-table__num">{{ amount(day.amount) }}</td>
|
<tr
|
||||||
<td class="rr-table__num">
|
class="rr-line"
|
||||||
@if (day.amount < m.dailyMinimumAmount) {
|
[class.rr-line--unconfigured]="!line.configured"
|
||||||
<span class="rr-badge rr-badge--unmapped">Below floor</span>
|
[class.rr-line--derived]="line.derived"
|
||||||
} @else {
|
>
|
||||||
<span class="rr-ok">OK</span>
|
<td>
|
||||||
|
<span class="rr-line__label">{{ line.label }}</span>
|
||||||
|
@if (!line.configured) {
|
||||||
|
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="rr-line__date">{{ line.asOnDate }}</td>
|
||||||
|
<td class="rr-table__num">
|
||||||
|
@if (line.configured) {
|
||||||
|
{{ amount(line.amount) }}
|
||||||
|
} @else {
|
||||||
|
<span class="rr-dash">—</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Account-level breakdown, so every total traces back to the accounts behind it. -->
|
||||||
|
@for (account of line.accounts; track account.glAccountId) {
|
||||||
|
<tr class="rr-subline" [class.rr-subline--excluded]="account.excluded">
|
||||||
|
<td>
|
||||||
|
<span class="rr-subline__code">{{ account.glCode }}</span>
|
||||||
|
{{ account.name }}
|
||||||
|
<!-- An account still pinned to the role but out of the total: named here so the
|
||||||
|
line's figure can be read as short by it, rather than looking complete. -->
|
||||||
|
@if (account.excluded) {
|
||||||
|
<span class="rr-subline__excluded">excluded — {{ account.excluded }}</span>
|
||||||
|
}
|
||||||
|
<!-- Where only part of an account is taken, the figure below is not the account's
|
||||||
|
balance and must not be read as one: state the whole balance and the share of
|
||||||
|
it this head takes, so the two can be reconciled without opening the ledger. -->
|
||||||
|
@if (isPartial(account)) {
|
||||||
|
<span class="rr-subline__portion">
|
||||||
|
{{ account.weightPercentage }}% of {{ amount(account.accountBalance) }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<!-- Which head the portion falls under, on a liability line that carries both. -->
|
||||||
|
@if (account.liabilityClass) {
|
||||||
|
<span class="rr-subline__class">{{ account.liabilityClass | titlecase }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="rr-table__num">{{ amount(account.balance) }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (data.computable) {
|
||||||
|
<tr class="rr-total">
|
||||||
|
<td>{{ data.rateType }} required ({{ data.ratePercentage }}% of NDTL)</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="rr-table__num">{{ amount(data.requiredAmount) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="rr-total">
|
||||||
|
<td>{{ data.rateType }} maintained</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="rr-table__num">{{ amount(data.maintainedAmount) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="rr-total" [class.rr-total--bad]="isShortfall">
|
||||||
|
<td>{{ isShortfall ? 'Shortfall' : 'Surplus' }}</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="rr-table__num">{{ amount(shortfallAmount) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
<!-- ═══ Memorandum and Annexures ═══ -->
|
<!--
|
||||||
@for (section of namedSections; track section.title) {
|
═══ Fortnight maintenance ═══
|
||||||
@if (section.rows.length) {
|
CRR is not met by the closing day's balance: the requirement must be held on average across
|
||||||
|
the fortnight AND a daily minimum on every day of it. A bank can pass the closing position and
|
||||||
|
still fail, so both tests are stated.
|
||||||
|
-->
|
||||||
|
@if (data.maintenance; as m) {
|
||||||
<div class="rr-card rr-card--section">
|
<div class="rr-card rr-card--section">
|
||||||
<h3 class="rr-section__title">{{ section.title }}</h3>
|
<h3 class="rr-section__title">
|
||||||
<p class="rr-section__hint">{{ section.hint }}</p>
|
Daily maintenance — {{ m.periodLabel || 'this month' }}
|
||||||
|
@if (!m.fortnightComplete) {
|
||||||
|
<span class="rr-badge rr-badge--partial">In progress — {{ m.dayCount }} days so far</span>
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
<p class="rr-section__hint">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="rr-summary">
|
||||||
|
<div class="rr-stat" [class.rr-stat--good]="m.averageMet" [class.rr-stat--bad]="!m.averageMet">
|
||||||
|
<span class="rr-stat__label">Average maintained</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(m.average) }}</span>
|
||||||
|
<span class="rr-stat__sub">
|
||||||
|
{{ m.averageMet ? 'Meets' : 'Below' }} required {{ amount(m.requiredAverage) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat" [class.rr-stat--good]="m.dailyMinimumMet" [class.rr-stat--bad]="!m.dailyMinimumMet">
|
||||||
|
<span class="rr-stat__label">Lowest day</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(m.lowest) }}</span>
|
||||||
|
<span class="rr-stat__sub"> on {{ m.lowestOn }} · floor {{ amount(m.dailyMinimumAmount) }} </span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat">
|
||||||
|
<span class="rr-stat__label">Days counted</span>
|
||||||
|
<span class="rr-stat__value">{{ m.dayCount }}</span>
|
||||||
|
<span class="rr-stat__sub">{{ m.fortnightComplete ? 'Full fortnight' : 'To reporting date' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="rr-table">
|
<table class="rr-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Particulars</th>
|
<th>Date</th>
|
||||||
<th class="rr-table__num">Amount (INR)</th>
|
<th class="rr-table__num">Required (INR)</th>
|
||||||
|
<th class="rr-table__num">Actual (INR)</th>
|
||||||
|
<th class="rr-table__num">Surplus / (Shortfall)</th>
|
||||||
|
<th class="rr-table__num">Against floor</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (row of section.rows; track row.configKey) {
|
@for (day of m.days; track day.date) {
|
||||||
<tr [class.rr-line--unconfigured]="!row.configured">
|
<tr [class.rr-day--short]="day.amount < m.dailyMinimumAmount">
|
||||||
<td>
|
<td>{{ day.date }}</td>
|
||||||
{{ row.label }}
|
<td class="rr-table__num">{{ amount(day.required) }}</td>
|
||||||
@if (!row.configured) {
|
<td class="rr-table__num">{{ amount(day.amount) }}</td>
|
||||||
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
<td class="rr-table__num" [class.rr-neg]="day.surplus < 0">{{ amount(day.surplus) }}</td>
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td class="rr-table__num">
|
<td class="rr-table__num">
|
||||||
@if (row.configured) {
|
@if (day.amount < m.dailyMinimumAmount) {
|
||||||
{{ amount(row.amount) }}
|
<span class="rr-badge rr-badge--unmapped">Below floor</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="rr-dash">—</span>
|
<span class="rr-ok">OK</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -311,6 +305,250 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- ═══ Memorandum and Annexures ═══ -->
|
||||||
|
@for (section of namedSections; track section.title) {
|
||||||
|
@if (section.rows.length) {
|
||||||
|
<div class="rr-card rr-card--section">
|
||||||
|
<h3 class="rr-section__title">{{ section.title }}</h3>
|
||||||
|
<p class="rr-section__hint">{{ section.hint }}</p>
|
||||||
|
<table class="rr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th class="rr-table__num">Amount (INR)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (row of section.rows; track row.configKey) {
|
||||||
|
<tr [class.rr-line--unconfigured]="!row.configured">
|
||||||
|
<td>
|
||||||
|
{{ row.label }}
|
||||||
|
@if (!row.configured) {
|
||||||
|
<span class="rr-badge rr-badge--unmapped">Not configured</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="rr-table__num">
|
||||||
|
@if (row.configured) {
|
||||||
|
{{ amount(row.amount) }}
|
||||||
|
} @else {
|
||||||
|
<span class="rr-dash">—</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
═══ Saved returns ═══
|
||||||
|
Outside the `@if (data)` block so the list stays reachable even when the live return cannot be
|
||||||
|
computed — a saved return from a month when it could is exactly what is wanted then, and a tab
|
||||||
|
that vanishes when the live figures fail would hide exactly the returns that still work.
|
||||||
|
-->
|
||||||
|
@if (activeTab === 'saved') {
|
||||||
|
<div class="rr-card rr-card--section rr-noprint">
|
||||||
|
<h3 class="rr-section__title">
|
||||||
|
Saved returns
|
||||||
|
@if (savedReturns.length) {
|
||||||
|
<span class="rr-badge rr-badge--mapped">{{ savedReturns.length }}</span>
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
<p class="rr-section__hint">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (data?.computable) {
|
||||||
|
<div class="rr-save-row">
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rr-save-row__note">
|
||||||
|
<mat-label>Note (optional)</mat-label>
|
||||||
|
<input matInput [(ngModel)]="savedRemarks" placeholder="e.g. provisional filing for this fortnight" />
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-raised-button color="primary" [disabled]="saving" (click)="saveCurrent()">
|
||||||
|
{{ saving ? 'Saving…' : 'Save this return' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (savedLoading) {
|
||||||
|
<div class="rr-loading">Loading saved returns…</div>
|
||||||
|
} @else if (!savedReturns.length) {
|
||||||
|
<div class="rr-empty">Nothing saved yet. Generate a return and press Save to keep it.</div>
|
||||||
|
} @else {
|
||||||
|
<table class="rr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Reporting date</th>
|
||||||
|
<th>Period</th>
|
||||||
|
<th class="rr-table__num">NDTL</th>
|
||||||
|
<th class="rr-table__num">Required</th>
|
||||||
|
<th class="rr-table__num">Maintained</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Saved</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (row of savedReturns; track row.id) {
|
||||||
|
<tr [class.rr-saved--open]="viewingSaved?.id === row.id">
|
||||||
|
<td>{{ row.reportingDate }}</td>
|
||||||
|
<td class="rr-saved__period">{{ row.periodStart }} → {{ row.periodEnd }}</td>
|
||||||
|
<td class="rr-table__num">{{ amount(row.ndtlAmount) }}</td>
|
||||||
|
<td class="rr-table__num">{{ amount(row.requiredAmount) }}</td>
|
||||||
|
<td class="rr-table__num">{{ amount(row.maintainedAmount) }}</td>
|
||||||
|
<td>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rr-saved__status">
|
||||||
|
<mat-select [value]="row.status" (selectionChange)="updateSavedStatus(row, $event.value)">
|
||||||
|
@for (s of filingStatuses; track s) {
|
||||||
|
<mat-option [value]="s">{{ statusLabel(s) }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</td>
|
||||||
|
<td class="rr-saved__meta">{{ row.createdOn }}<br />{{ row.createdBy }}</td>
|
||||||
|
<td class="rr-saved__actions">
|
||||||
|
<button mat-button class="rr-saved__view" (click)="viewSaved(row)">
|
||||||
|
{{ viewingSaved?.id === row.id ? 'Close' : 'View' }}
|
||||||
|
</button>
|
||||||
|
<button mat-button class="rr-saved__del" (click)="deleteSaved(row)" title="Remove">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
═══ The opened snapshot ═══
|
||||||
|
Its own section below the list rather than in place of the live return: the reason to open a
|
||||||
|
saved return is almost always to compare it against where the bank stands now, which swapping
|
||||||
|
the live figures out would make impossible.
|
||||||
|
-->
|
||||||
|
@if (viewingSaved && savedData; as _) {
|
||||||
|
<div class="rr-card rr-card--section rr-card--saved" id="rr-saved-view">
|
||||||
|
<div class="rr-saved-head">
|
||||||
|
<div>
|
||||||
|
<h3 class="rr-section__title">
|
||||||
|
Saved return — {{ savedData.reportingDate }}
|
||||||
|
<span class="rr-badge rr-badge--future">{{ statusLabel(viewingSaved.status) }}</span>
|
||||||
|
</h3>
|
||||||
|
<p class="rr-section__hint">
|
||||||
|
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) {
|
||||||
|
<em class="rr-saved__remarks">“{{ viewingSaved.remarks }}”</em>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button mat-stroked-button (click)="closeSaved()">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rr-summary">
|
||||||
|
<div class="rr-stat">
|
||||||
|
<span class="rr-stat__label">NDTL base</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(savedData.ndtlAmount ?? savedData.dtlAmount) }}</span>
|
||||||
|
<span class="rr-stat__sub">as at {{ savedData.basis.dtlAsOnDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat">
|
||||||
|
<span class="rr-stat__label">{{ savedData.rateType }} rate</span>
|
||||||
|
<span class="rr-stat__value">{{ savedData.ratePercentage }}%</span>
|
||||||
|
<span class="rr-stat__sub">as filed</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat">
|
||||||
|
<span class="rr-stat__label">Required</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(savedData.requiredAmount) }}</span>
|
||||||
|
<span class="rr-stat__sub">{{ savedData.ratePercentage }}% of NDTL</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat">
|
||||||
|
<span class="rr-stat__label">Maintained</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(savedData.maintainedAmount) }}</span>
|
||||||
|
<span class="rr-stat__sub">on {{ savedData.basis.holdingsAsOnDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rr-stat" [class.rr-stat--good]="savedData.compliant" [class.rr-stat--bad]="!savedData.compliant">
|
||||||
|
<span class="rr-stat__label">{{ savedData.compliant ? 'Surplus' : 'Shortfall' }}</span>
|
||||||
|
<span class="rr-stat__value">{{ amount(savedSurplus) }}</span>
|
||||||
|
<span class="rr-stat__sub">{{ savedData.compliant ? 'Above requirement' : 'Below requirement' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="rr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th>As at</th>
|
||||||
|
<th class="rr-table__num">Amount (INR)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (line of savedData.lines; track line.configKey) {
|
||||||
|
<tr class="rr-line" [class.rr-line--unconfigured]="!line.configured">
|
||||||
|
<td>
|
||||||
|
<span class="rr-line__label">{{ line.label }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="rr-line__date">{{ line.asOnDate }}</td>
|
||||||
|
<td class="rr-table__num">
|
||||||
|
@if (line.configured) {
|
||||||
|
{{ amount(line.amount) }}
|
||||||
|
} @else {
|
||||||
|
<span class="rr-dash">—</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@for (account of line.accounts; track account.glAccountId) {
|
||||||
|
<tr class="rr-subline">
|
||||||
|
<td>
|
||||||
|
<span class="rr-subline__code">{{ account.glCode }}</span>
|
||||||
|
{{ account.name }}
|
||||||
|
@if (isPartial(account)) {
|
||||||
|
<span class="rr-subline__portion">
|
||||||
|
{{ account.weightPercentage }}% of {{ amount(account.accountBalance) }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (account.liabilityClass) {
|
||||||
|
<span class="rr-subline__class">{{ account.liabilityClass | titlecase }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="rr-table__num">{{ amount(account.balance) }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
@if (savedData.maintenance; as sm) {
|
||||||
|
<h4 class="rr-saved__subhead">Daily maintenance as filed — {{ sm.periodLabel || '' }}</h4>
|
||||||
|
<table class="rr-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th class="rr-table__num">Required</th>
|
||||||
|
<th class="rr-table__num">Actual</th>
|
||||||
|
<th class="rr-table__num">Surplus / (Shortfall)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (day of sm.days; track day.date) {
|
||||||
|
<tr [class.rr-day--short]="day.amount < sm.dailyMinimumAmount">
|
||||||
|
<td>{{ day.date }}</td>
|
||||||
|
<td class="rr-table__num">{{ amount(day.required) }}</td>
|
||||||
|
<td class="rr-table__num">{{ amount(day.amount) }}</td>
|
||||||
|
<td class="rr-table__num" [class.rr-neg]="day.surplus < 0">{{ amount(day.surplus) }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,52 @@ $red: #c5221f;
|
|||||||
width: 180px;
|
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 ──
|
// ── Basis ──
|
||||||
|
|
||||||
.rr-basis {
|
.rr-basis {
|
||||||
@@ -297,7 +343,7 @@ $red: #c5221f;
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
padding: 10px 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid $border;
|
border-bottom: 1px solid $border;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -446,6 +492,154 @@ $red: #c5221f;
|
|||||||
font-weight: 600;
|
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 {
|
.rr-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -479,7 +673,7 @@ $red: #c5221f;
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (width <= 720px) {
|
||||||
.rr-header__actions {
|
.rr-header__actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -21,7 +21,7 @@
|
|||||||
<div class="flex-100 layout-row-wrap responsive-column">
|
<div class="flex-100 layout-row-wrap responsive-column">
|
||||||
<mifosx-gl-account-display
|
<mifosx-gl-account-display
|
||||||
class="flex-100"
|
class="flex-100"
|
||||||
[accountTitle]="'Saving reference'"
|
[accountTitle]="productLabel ? productLabel + ' reference' : 'Saving reference'"
|
||||||
[glAccount]="accountingMappings.savingsReferenceAccount"
|
[glAccount]="accountingMappings.savingsReferenceAccount"
|
||||||
[withTitle]="'40%'"
|
[withTitle]="'40%'"
|
||||||
>
|
>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<div class="flex-100 layout-row-wrap responsive-column">
|
<div class="flex-100 layout-row-wrap responsive-column">
|
||||||
<mifosx-gl-account-display
|
<mifosx-gl-account-display
|
||||||
class="flex-100"
|
class="flex-100"
|
||||||
[accountTitle]="'Saving control'"
|
[accountTitle]="productLabel ? productLabel + ' control' : 'Saving control'"
|
||||||
[glAccount]="accountingMappings.savingsControlAccount"
|
[glAccount]="accountingMappings.savingsControlAccount"
|
||||||
[withTitle]="'40%'"
|
[withTitle]="'40%'"
|
||||||
>
|
>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<div class="flex-100 layout-row-wrap responsive-column">
|
<div class="flex-100 layout-row-wrap responsive-column">
|
||||||
<mifosx-gl-account-display
|
<mifosx-gl-account-display
|
||||||
class="flex-100"
|
class="flex-100"
|
||||||
[accountTitle]="'Savings transfers in suspense'"
|
[accountTitle]="productLabel ? 'Transfers in suspense' : 'Savings transfers in suspense'"
|
||||||
[glAccount]="accountingMappings.transfersInSuspenseAccount"
|
[glAccount]="accountingMappings.transfersInSuspenseAccount"
|
||||||
[withTitle]="'40%'"
|
[withTitle]="'40%'"
|
||||||
>
|
>
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
<div class="flex-100 layout-row-wrap responsive-column">
|
<div class="flex-100 layout-row-wrap responsive-column">
|
||||||
<mifosx-gl-account-display
|
<mifosx-gl-account-display
|
||||||
class="flex-100"
|
class="flex-100"
|
||||||
[accountTitle]="'Interest on savings'"
|
[accountTitle]="productLabel ? 'Interest on ' + productLabel.toLowerCase() + 's' : 'Interest on savings'"
|
||||||
[glAccount]="accountingMappings.interestOnSavingsAccount"
|
[glAccount]="accountingMappings.interestOnSavingsAccount"
|
||||||
[withTitle]="'40%'"
|
[withTitle]="'40%'"
|
||||||
>
|
>
|
||||||
|
|||||||
+2
@@ -26,6 +26,8 @@ export class ViewSavingsAccountingDetailsComponent {
|
|||||||
|
|
||||||
@Input() accountingRule: OptionData;
|
@Input() accountingRule: OptionData;
|
||||||
@Input() accountingMappings: any[] = [];
|
@Input() accountingMappings: any[] = [];
|
||||||
|
/** Product name for GL row titles (e.g. 'Fixed deposit'); omitted = savings wording. */
|
||||||
|
@Input() productLabel?: string;
|
||||||
|
|
||||||
isCashOrAccrualAccounting(): boolean {
|
isCashOrAccrualAccounting(): boolean {
|
||||||
if (this.accountingRule) {
|
if (this.accountingRule) {
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@
|
|||||||
// Tenants the login screen offers, and which one it starts on. Every identifier here has to exist
|
// Tenants the login screen offers, and which one it starts on. Every identifier here has to exist
|
||||||
// in the tenants table of the tenant-store database, or the platform rejects the request as an
|
// in the tenants table of the tenant-store database, or the platform rejects the request as an
|
||||||
// unknown tenant.
|
// unknown tenant.
|
||||||
window["env"]["fineractPlatformTenantId"] = 'lincpay';
|
window["env"]["fineractPlatformTenantId"] = 'utkarshbank';
|
||||||
window["env"]["fineractPlatformTenantIds"] = 'lincpay';
|
window["env"]["fineractPlatformTenantIds"] = 'utkarshbank';
|
||||||
|
|
||||||
window['env']['tenantLogoUrl'] = '';
|
window['env']['tenantLogoUrl'] = '';
|
||||||
window['env']['tenantLogoUrlDark'] = '';
|
window['env']['tenantLogoUrlDark'] = '';
|
||||||
|
|||||||
@@ -2236,6 +2236,9 @@
|
|||||||
"Interest free period": "Interest free period",
|
"Interest free period": "Interest free period",
|
||||||
"Interest method": "Interest method",
|
"Interest method": "Interest method",
|
||||||
"Interest on savings": "Interest on savings",
|
"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 rate": "Interest Rate",
|
||||||
"Interest recalculation compounding on": "Interest recalculation compounding on",
|
"Interest recalculation compounding on": "Interest recalculation compounding on",
|
||||||
"Interest waived on": "Interest waived on",
|
"Interest waived on": "Interest waived on",
|
||||||
@@ -2793,6 +2796,7 @@
|
|||||||
"Savings Product": "Savings Product",
|
"Savings Product": "Savings Product",
|
||||||
"Savings SubType": "Savings SubType",
|
"Savings SubType": "Savings SubType",
|
||||||
"Savings transfers in suspense": "Savings transfers in suspense",
|
"Savings transfers in suspense": "Savings transfers in suspense",
|
||||||
|
"Transfers in suspense": "Transfers in suspense",
|
||||||
"Schedule Date": "Schedule Date",
|
"Schedule Date": "Schedule Date",
|
||||||
"Share Details": "Share Details",
|
"Share Details": "Share Details",
|
||||||
"Scheduler Jobs": "Scheduler Jobs",
|
"Scheduler Jobs": "Scheduler Jobs",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260924',
|
'version': '260928',
|
||||||
'hash': 'a6e62201'
|
'hash': '6cbb31b5'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export const environment = {
|
|||||||
version: env.mifos_x.version,
|
version: env.mifos_x.version,
|
||||||
hash: env.mifos_x.hash,
|
hash: env.mifos_x.hash,
|
||||||
// For connecting to server running elsewhere update the tenant identifier
|
// For connecting to server running elsewhere update the tenant identifier
|
||||||
fineractPlatformTenantId: loadedEnv['fineractPlatformTenantId'] || 'lincpay',
|
fineractPlatformTenantId: loadedEnv['fineractPlatformTenantId'] || 'utkarshbank',
|
||||||
fineractPlatformTenantIds: loadedEnv['fineractPlatformTenantIds'] || 'lincpay',
|
fineractPlatformTenantIds: loadedEnv['fineractPlatformTenantIds'] || 'utkarshbank',
|
||||||
// For connecting to others servers running elsewhere update the base API URL
|
// For connecting to others servers running elsewhere update the base API URL
|
||||||
baseApiUrls:
|
baseApiUrls:
|
||||||
loadedEnv['fineractApiUrls'] || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,
|
loadedEnv['fineractApiUrls'] || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const environment = {
|
|||||||
version: env.mifos_x.version,
|
version: env.mifos_x.version,
|
||||||
hash: env.mifos_x.hash,
|
hash: env.mifos_x.hash,
|
||||||
// For connecting to server running elsewhere update the tenant identifier
|
// For connecting to server running elsewhere update the tenant identifier
|
||||||
fineractPlatformTenantId: loadedEnv.fineractPlatformTenantId || 'lincpay',
|
fineractPlatformTenantId: loadedEnv.fineractPlatformTenantId || 'utkarshbank',
|
||||||
fineractPlatformTenantIds: loadedEnv.fineractPlatformTenantIds || 'lincpay',
|
fineractPlatformTenantIds: loadedEnv.fineractPlatformTenantIds || 'utkarshbank',
|
||||||
// For connecting to others servers running elsewhere update the base API URL
|
// For connecting to others servers running elsewhere update the base API URL
|
||||||
baseApiUrls:
|
baseApiUrls:
|
||||||
loadedEnv.fineractApiUrls || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,
|
loadedEnv.fineractApiUrls || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,
|
||||||
|
|||||||
Reference in New Issue
Block a user