/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Component, OnInit, ViewChild, inject } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, MatSortHeader } from '@angular/material/sort'; import { MatTableDataSource, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; import { NgClass } from '@angular/common'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** Category icon/color mapping */ const CATEGORY_CONFIG: Record = { Client: { icon: 'fa-users', color: 'blue' }, Loan: { icon: 'fa-credit-card', color: 'green' }, Savings: { icon: 'fa-piggy-bank', color: 'teal' }, Fund: { icon: 'fa-hand-holding-usd', color: 'amber' }, Accounting: { icon: 'fa-calculator', color: 'purple' }, Aging: { icon: 'fa-clock', color: 'red' }, 'Aging(Arrears)': { icon: 'fa-exclamation-triangle', color: 'red' }, SMS: { icon: 'fa-sms', color: 'green' }, 'Loans Pending Approval': { icon: 'fa-hourglass-half', color: 'amber' }, 'General Reports': { icon: 'fa-file-alt', color: 'purple' } }; const DEFAULT_CONFIG = { icon: 'fa-chart-bar', color: 'blue' }; /** Sub-category rules for large categories (keyword → sub-category name) */ const SUB_CATEGORY_RULES: Record = { Loan: [ { name: 'Disbursal & Approval', keywords: ['disbursal', 'pending approval', 'awaiting'], icon: 'fa-check-circle', color: 'green' }, { name: 'Active & Portfolio', keywords: ['active loan', 'portfolio', 'outstanding', 'balance', 'summary'], icon: 'fa-chart-line', color: 'blue' }, { name: 'Arrears & Aging', keywords: ['arrear', 'aging', 'overdue', 'delinquen', 'written off', 'write-off', 'obligation'], icon: 'fa-exclamation-triangle', color: 'red' }, { name: 'Repayment & Collection', keywords: ['repayment', 'collection', 'demand', 'due', 'paid', 'payment', 'transaction'], icon: 'fa-money-bill', color: 'teal' }, { name: 'Other Loan Reports', keywords: [], icon: 'fa-file-alt', color: 'purple' } ] }; interface SubCat { name: string; count: number; icon: string; color: string; reports: any[]; } @Component({ selector: 'mifosx-reports', templateUrl: './reports.component.html', styleUrls: ['./reports.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, MatTable, MatSort, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatSortHeader, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatPaginator, NgClass ] }) export class ReportsComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); reportsData: any[] = []; filter: string; displayedColumns: string[] = ['reportName', 'reportType', 'reportCategory']; dataSource = new MatTableDataSource(); /** Category dashboard */ categories: { name: string; count: number; icon: string; color: string }[] = []; selectedCategory: string | null = null; filteredReports: any[] = []; isSearching = false; /** Sub-categories (for large categories) */ subCategories: SubCat[] = []; selectedSubCategory: string | null = null; showSubCategories = false; @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; @ViewChild(MatSort, { static: true }) sort: MatSort; constructor() { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.route.data.subscribe((data: { reports: any }) => { this.reportsData = data.reports || []; }); this.filter = this.route.snapshot.params['filter']; } ngOnInit() { this.buildCategories(); if (this.filter) { this.selectCategory(this.filter); } this.dataSource = new MatTableDataSource(this.reportsData); this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } private isNullCategory(c: any): boolean { return !c || (typeof c === 'string' && (c.toUpperCase() === 'NULL' || c === '(NULL)')); } buildCategories() { const catMap = new Map(); for (const report of this.reportsData) { const cat = this.isNullCategory(report.reportCategory) ? 'General Reports' : report.reportCategory; catMap.set(cat, (catMap.get(cat) || 0) + 1); } this.categories = Array.from(catMap.entries()) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => { const cfg = CATEGORY_CONFIG[name] || DEFAULT_CONFIG; return { name, count, icon: cfg.icon, color: cfg.color }; }); } selectCategory(category: string) { this.selectedCategory = category; this.selectedSubCategory = null; if (category === 'General Reports') { this.filteredReports = this.reportsData.filter((r: any) => this.isNullCategory(r.reportCategory)); } else { this.filteredReports = this.reportsData.filter((r: any) => r.reportCategory === category); } // Check if this category has sub-category rules const rules = SUB_CATEGORY_RULES[category]; if (rules && this.filteredReports.length > 15) { this.buildSubCategories(rules); this.showSubCategories = true; } else { this.showSubCategories = false; this.setTableData(this.filteredReports); } this.isSearching = false; } private buildSubCategories(rules: { name: string; keywords: string[]; icon: string; color: string }[]) { const assigned = new Set(); this.subCategories = []; for (const rule of rules) { if (rule.keywords.length === 0) continue; // "Other" bucket — handle last const matched = this.filteredReports.filter((r: any, idx: number) => { if (assigned.has(idx)) return false; const name = (r.reportName || '').toLowerCase(); return rule.keywords.some(kw => name.includes(kw)); }); matched.forEach(r => assigned.add(this.filteredReports.indexOf(r))); if (matched.length > 0) { this.subCategories.push({ name: rule.name, count: matched.length, icon: rule.icon, color: rule.color, reports: matched }); } } // "Other" bucket — remaining unmatched const otherRule = rules.find(r => r.keywords.length === 0); const remaining = this.filteredReports.filter((_, idx) => !assigned.has(idx)); if (remaining.length > 0 && otherRule) { this.subCategories.push({ name: otherRule.name, count: remaining.length, icon: otherRule.icon, color: otherRule.color, reports: remaining }); } } selectSubCategory(subCat: SubCat) { this.selectedSubCategory = subCat.name; this.showSubCategories = false; this.setTableData(subCat.reports); } backToSubCategories() { this.selectedSubCategory = null; this.showSubCategories = true; } clearCategory() { this.selectedCategory = null; this.selectedSubCategory = null; this.showSubCategories = false; this.isSearching = false; this.setTableData(this.reportsData); } private setTableData(data: any[]) { this.dataSource = new MatTableDataSource(data); this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } applyFilter(filterValue: string) { if (filterValue.trim().length > 0) { this.isSearching = true; this.showSubCategories = false; if (this.selectedSubCategory) { const subCat = this.subCategories.find(sc => sc.name === this.selectedSubCategory); this.setTableData(subCat ? subCat.reports : this.filteredReports); } else if (this.selectedCategory) { this.setTableData(this.filteredReports); } else { this.setTableData(this.reportsData); } this.dataSource.filter = filterValue.trim().toLowerCase(); } else { if (this.selectedSubCategory) { // keep showing sub-cat table } else if (this.selectedCategory && SUB_CATEGORY_RULES[this.selectedCategory] && this.filteredReports.length > 15) { this.isSearching = false; this.showSubCategories = true; } else { this.isSearching = false; } } } }