/** * 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; /** 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 { 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 { 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 { 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 { 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' }); } }