262 lines
8.6 KiB
TypeScript
262 lines
8.6 KiB
TypeScript
/**
|
|
* 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 { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
|
|
import { Router, RouterLink } from '@angular/router';
|
|
import { Observable } from 'rxjs';
|
|
import { map } from 'rxjs/operators';
|
|
import { activities } from '../activities';
|
|
import { MatAutocompleteTrigger, MatAutocomplete, MatOption } from '@angular/material/autocomplete';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
import { AsyncPipe, DatePipe } from '@angular/common';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { RbiCircularsService } from '../../rbi-circulars/rbi-circulars.service';
|
|
/**
|
|
* Dashboard component.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-dashboard',
|
|
standalone: true,
|
|
templateUrl: './dashboard.component.html',
|
|
styleUrls: ['./dashboard.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatAutocompleteTrigger,
|
|
MatAutocomplete,
|
|
MatTooltipModule,
|
|
AsyncPipe,
|
|
DatePipe,
|
|
FaIconComponent
|
|
]
|
|
})
|
|
export class DashboardComponent implements OnInit {
|
|
private router = inject(Router);
|
|
private dialog = inject(MatDialog);
|
|
private rbi = inject(RbiCircularsService);
|
|
|
|
today = new Date();
|
|
|
|
/**
|
|
* The CRR/SLR tile, read from the return.
|
|
*
|
|
* The amounts and the rates were both fixed text on the template until now, so the tile reported
|
|
* ₹2,077 Cr at 3% whatever the ledger held and whatever rate Admin was on. A dashboard figure is
|
|
* read as the bank's position, and one that cannot move is worse than none: nothing on the tile
|
|
* says it is a placeholder.
|
|
*
|
|
* Dashes until the return answers. 'status' drives the compliance tag, which is only claimed
|
|
* where the return states it.
|
|
*/
|
|
crrSlr: {
|
|
crrRate: number | null;
|
|
slrRate: number | null;
|
|
crrRequired: string;
|
|
slrRequired: string;
|
|
status: 'ok' | 'short' | 'unknown';
|
|
} = { crrRate: null, slrRate: null, crrRequired: '—', slrRequired: '—', status: 'unknown' };
|
|
|
|
/** Array of all user activities */
|
|
userActivity: string[];
|
|
/** Array of most recent user activities */
|
|
recentActivities: string[];
|
|
/** Array of most frequent user activities */
|
|
frequentActivities: string[];
|
|
/** Search Text. */
|
|
searchText: UntypedFormControl = new UntypedFormControl();
|
|
/** Filtered Activities. */
|
|
filteredActivities: Observable<any[]>;
|
|
/** All User Activities. */
|
|
allActivities: any[] = activities;
|
|
|
|
/**
|
|
* Gets user activities from local storage.
|
|
*/
|
|
constructor() {
|
|
this.userActivity = JSON.parse(localStorage.getItem('mifosXLocation')) || [];
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.recentActivities = this.getRecentActivities();
|
|
this.frequentActivities = this.getFrequentActivities();
|
|
this.setFilteredActivities();
|
|
this.loadCrrSlr();
|
|
}
|
|
|
|
/** Fills the CRR/SLR tile from the return, leaving it dashed where the return cannot compute. */
|
|
private loadCrrSlr(): void {
|
|
const crore = (v: number | null | undefined): string =>
|
|
v === null || v === undefined ? '—' : `₹${Math.round(v / 1e7).toLocaleString('en-IN')} Cr`;
|
|
|
|
// The statutory Form 1, which is the return this bank files — twice a month, at the 15th and at
|
|
// the month end. The working report's 45-day lag lands mid-fortnight, on a date the bank never
|
|
// reported, so a tile built on it shows a position that appears on no filing.
|
|
this.rbi.getForm1Statutory().subscribe({
|
|
next: (data: any) => {
|
|
const latest = (data?.columns?.length ?? 0) - 1;
|
|
if (latest < 0) {
|
|
return;
|
|
}
|
|
this.crrSlr.crrRate = data?.crrPercentage ?? null;
|
|
this.crrSlr.slrRate = data?.slrPercentage ?? null;
|
|
this.crrSlr.crrRequired = crore(data?.partB?.required?.[latest]);
|
|
this.crrSlr.slrRequired = crore(data?.partC?.required?.[latest]);
|
|
|
|
// Compliant only where BOTH ratios are met: the tile carries one tag for the pair, and a
|
|
// bank short on SLR is not within limits because its cash reserve happens to be ample.
|
|
const crrHeld = data?.partB?.maintained?.[latest];
|
|
const crrNeed = data?.partB?.required?.[latest];
|
|
const slrHeld = data?.partC?.maintained?.[latest];
|
|
const slrNeed = data?.partC?.required?.[latest];
|
|
if ([
|
|
crrHeld,
|
|
crrNeed,
|
|
slrHeld,
|
|
slrNeed
|
|
].every((v) => v !== null && v !== undefined)) {
|
|
this.crrSlr.status = crrHeld >= crrNeed && slrHeld >= slrNeed ? 'ok' : 'short';
|
|
}
|
|
},
|
|
error: () => undefined
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns top eight recent activities.
|
|
*/
|
|
getRecentActivities() {
|
|
const reverseActivities = this.userActivity.reverse();
|
|
const uniqueActivities: string[] = [];
|
|
reverseActivities.forEach((activity: string) => {
|
|
if (!uniqueActivities.includes(activity)) {
|
|
uniqueActivities.push(activity);
|
|
}
|
|
});
|
|
const topEightRecentActivities = uniqueActivities
|
|
.filter(
|
|
(activity: string) => ![
|
|
'/',
|
|
'/login',
|
|
'/home',
|
|
'/dashboard'
|
|
].includes(activity)
|
|
)
|
|
.slice(0, 8);
|
|
return topEightRecentActivities;
|
|
}
|
|
|
|
/**
|
|
* Returns top eight frequent activities.
|
|
*/
|
|
getFrequentActivities() {
|
|
const frequencyCounts: any = {};
|
|
let index = this.userActivity?.length;
|
|
while (index) {
|
|
const activity = this.userActivity[--index];
|
|
frequencyCounts[activity] = (frequencyCounts[activity] || 0) + 1;
|
|
}
|
|
const frequencyCountsArray = Object.entries(frequencyCounts);
|
|
const topEigthFrequentActivities = frequencyCountsArray
|
|
.sort((a: any, b: any) => b[1] - a[1])
|
|
.map((entry: any[]) => entry[0])
|
|
.filter(
|
|
(activity: string) => ![
|
|
'/',
|
|
'/login',
|
|
'/home',
|
|
'/dashboard'
|
|
].includes(activity)
|
|
)
|
|
.slice(0, 8);
|
|
return topEigthFrequentActivities;
|
|
}
|
|
|
|
/**
|
|
* Navigates to the activity
|
|
*/
|
|
navigatetoActivity(activity: string) {
|
|
this.router.navigateByUrl(activity);
|
|
}
|
|
|
|
/**
|
|
* Sets filtered activities for autocomplete.
|
|
*/
|
|
setFilteredActivities() {
|
|
this.filteredActivities = this.searchText.valueChanges.pipe(
|
|
map((activity: any) => (typeof activity === 'string' ? activity : activity.activity)),
|
|
map((activityName: string) => (activityName ? this.filterActivity(activityName) : this.allActivities))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filters activities.
|
|
* @param activityName Activity name to filter activity by.
|
|
* @returns {any} Filtered activities.
|
|
*/
|
|
private filterActivity(activityName: string): any {
|
|
const filterValue = activityName.toLowerCase();
|
|
return this.allActivities.filter((activity) => activity.activity.toLowerCase().indexOf(filterValue) === 0);
|
|
}
|
|
|
|
/**
|
|
* Opens the CRR/SLR details dialog with full liabilities breakdown.
|
|
*/
|
|
async openCrrSlrDialog(): Promise<void> {
|
|
const { CrrSlrDialogComponent } = await import('./crr-slr-dialog/crr-slr-dialog.component');
|
|
this.dialog.open(CrrSlrDialogComponent, {
|
|
width: '95vw',
|
|
maxWidth: '1100px',
|
|
maxHeight: '90vh',
|
|
panelClass: 'crr-slr-dialog-panel',
|
|
autoFocus: false
|
|
});
|
|
}
|
|
|
|
async openCrarDialog(): Promise<void> {
|
|
const { CrarDialogComponent } = await import('./crar-dialog/crar-dialog.component');
|
|
this.dialog.open(CrarDialogComponent, {
|
|
width: '95vw',
|
|
maxWidth: '1360px',
|
|
maxHeight: '90vh',
|
|
panelClass: 'crr-slr-dialog-panel',
|
|
autoFocus: false
|
|
});
|
|
}
|
|
|
|
async openCicDqrDialog(): Promise<void> {
|
|
const { CicDqrDialogComponent } = await import('./cic-dqr-dialog/cic-dqr-dialog.component');
|
|
this.dialog.open(CicDqrDialogComponent, {
|
|
width: '88vw',
|
|
maxWidth: '1200px',
|
|
maxHeight: '92vh',
|
|
panelClass: 'crr-slr-dialog-panel',
|
|
autoFocus: false,
|
|
disableClose: false,
|
|
hasBackdrop: true,
|
|
backdropClass: 'cic-dlg-backdrop'
|
|
});
|
|
}
|
|
|
|
async openPslReportDialog(): Promise<void> {
|
|
const { PslReportDialogComponent } = await import('./psl-report-dialog/psl-report-dialog.component');
|
|
this.dialog.open(PslReportDialogComponent, {
|
|
width: '92vw',
|
|
maxWidth: '1280px',
|
|
maxHeight: '92vh',
|
|
panelClass: 'crr-slr-dialog-panel',
|
|
autoFocus: false,
|
|
disableClose: false,
|
|
hasBackdrop: true,
|
|
backdropClass: 'psl-dlg-backdrop'
|
|
});
|
|
}
|
|
}
|