@@ -50,7 +50,11 @@
|
||||
<div class="rpt-detail">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="rpt-detail__header">
|
||||
<button mat-stroked-button class="rpt-back-btn" (click)="selectedSubCategory ? backToSubCategories() : clearCategory()">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="rpt-back-btn"
|
||||
(click)="selectedSubCategory ? backToSubCategories() : clearCategory()"
|
||||
>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
{{ selectedSubCategory ? selectedCategory : 'All Categories' }}
|
||||
</button>
|
||||
@@ -65,7 +69,11 @@
|
||||
<div class="rpt-search-row">
|
||||
<mat-form-field appearance="outline" class="rpt-search">
|
||||
<i class="fas fa-search rpt-search__icon" matPrefix></i>
|
||||
<input matInput placeholder="Search in {{ selectedCategory }}..." (keyup)="applyFilter($event.target.value)" />
|
||||
<input
|
||||
matInput
|
||||
placeholder="Search in {{ selectedCategory }}..."
|
||||
(keyup)="applyFilter($event.target.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@@ -124,9 +132,13 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="rpt-row"
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
class="rpt-row"
|
||||
[routerLink]="['/reports', 'run', row.reportName]"
|
||||
[queryParams]="{ type: row.reportType, id: row.id }"></tr>
|
||||
[queryParams]="{ type: row.reportType, id: row.id }"
|
||||
></tr>
|
||||
</table>
|
||||
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
@@ -60,10 +60,21 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
span {
|
||||
border-radius: 2px;
|
||||
|
||||
&:nth-child(1) { background: $brand; }
|
||||
&:nth-child(2) { background: $accent; }
|
||||
&:nth-child(3) { background: #10b981; }
|
||||
&:nth-child(4) { background: #cbd5e1; }
|
||||
&:nth-child(1) {
|
||||
background: $brand;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,21 +94,33 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-form-field-flex { height: 38px; }
|
||||
.mat-mdc-form-field-flex {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-infix {
|
||||
padding: 6px 0;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-subscript-wrapper { height: 0; min-height: 0; }
|
||||
|
||||
.mdc-notched-outline {
|
||||
.mdc-notched-outline__leading { border-radius: 8px 0 0 8px; }
|
||||
.mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; }
|
||||
.mat-mdc-form-field-subscript-wrapper {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
input { font-size: 13px; }
|
||||
.mdc-notched-outline {
|
||||
.mdc-notched-outline__leading {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.mdc-notched-outline__trailing {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +130,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 900px) { grid-template-columns: repeat(2, 1fr); }
|
||||
@media (max-width: 500px) { grid-template-columns: 1fr; }
|
||||
@media (width <= 900px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (width <= 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.rpt-tile {
|
||||
@@ -117,13 +145,15 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgb(0 0 0 / 6%);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.15s, transform 0.15s;
|
||||
transition:
|
||||
box-shadow 0.15s,
|
||||
transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -137,12 +167,35 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.blue { background: #e0f2fe; color: #0284c7; }
|
||||
&.green { background: #d1fae5; color: #059669; }
|
||||
&.purple { background: #ede9fe; color: #7c3aed; }
|
||||
&.amber { background: #fef3c7; color: #d97706; }
|
||||
&.teal { background: #ccfbf1; color: #0d9488; }
|
||||
&.red { background: #fee2e2; color: #dc2626; }
|
||||
&.blue {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
&.amber {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
&.teal {
|
||||
background: #ccfbf1;
|
||||
color: #0d9488;
|
||||
}
|
||||
|
||||
&.red {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
@@ -171,13 +224,17 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
transition: color 0.12s;
|
||||
}
|
||||
|
||||
&:hover &__arrow { color: $brand; }
|
||||
&:hover &__arrow {
|
||||
color: $brand;
|
||||
}
|
||||
}
|
||||
|
||||
.rpt-tiles--sub {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// Detail view
|
||||
@@ -203,9 +260,14 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
border-color: $border !important;
|
||||
color: $text-muted !important;
|
||||
|
||||
i { margin-right: 6px; }
|
||||
i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover { color: $brand !important; border-color: $brand !important; }
|
||||
&:hover {
|
||||
color: $brand !important;
|
||||
border-color: $brand !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Table
|
||||
@@ -245,8 +307,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
height: 38px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:nth-child(even) { background: #fafbfc; }
|
||||
&:last-child { border-bottom: none; }
|
||||
&:nth-child(even) {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-cell {
|
||||
@@ -294,11 +361,22 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
||||
:host-context(.dark-theme) {
|
||||
.rpt-tile {
|
||||
background: #1e1e2e;
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
.rpt-tile__name { color: #e0e0e0; }
|
||||
.rpt-tile__arrow { color: #444; }
|
||||
.rpt-header__title { color: #e0e0e0; }
|
||||
.rpt-detail__title { color: #e0e0e0; }
|
||||
.rpt-tile__name {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.rpt-tile__arrow {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.rpt-header__title {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.rpt-detail__title {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,36 @@ const DEFAULT_CONFIG = { icon: 'fa-chart-bar', color: 'blue' };
|
||||
/** Sub-category rules for large categories (keyword → sub-category name) */
|
||||
const SUB_CATEGORY_RULES: Record<string, { name: string; keywords: string[]; icon: string; color: string }[]> = {
|
||||
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: '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' }
|
||||
]
|
||||
};
|
||||
@@ -89,7 +115,11 @@ export class ReportsComponent implements OnInit {
|
||||
|
||||
reportsData: any[] = [];
|
||||
filter: string;
|
||||
displayedColumns: string[] = ['reportName', 'reportType', 'reportCategory'];
|
||||
displayedColumns: string[] = [
|
||||
'reportName',
|
||||
'reportType',
|
||||
'reportCategory'
|
||||
];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
||||
/** Category dashboard */
|
||||
@@ -136,10 +166,15 @@ export class ReportsComponent implements OnInit {
|
||||
}
|
||||
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 };
|
||||
});
|
||||
.map(
|
||||
([
|
||||
name,
|
||||
count
|
||||
]) => {
|
||||
const cfg = CATEGORY_CONFIG[name] || DEFAULT_CONFIG;
|
||||
return { name, count, icon: cfg.icon, color: cfg.color };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
selectCategory(category: string) {
|
||||
@@ -173,19 +208,31 @@ export class ReportsComponent implements OnInit {
|
||||
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));
|
||||
return rule.keywords.some((kw) => name.includes(kw));
|
||||
});
|
||||
matched.forEach(r => assigned.add(this.filteredReports.indexOf(r)));
|
||||
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 });
|
||||
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 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 });
|
||||
this.subCategories.push({
|
||||
name: otherRule.name,
|
||||
count: remaining.length,
|
||||
icon: otherRule.icon,
|
||||
color: otherRule.color,
|
||||
reports: remaining
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +266,7 @@ export class ReportsComponent implements OnInit {
|
||||
this.isSearching = true;
|
||||
this.showSubCategories = false;
|
||||
if (this.selectedSubCategory) {
|
||||
const subCat = this.subCategories.find(sc => sc.name === 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);
|
||||
@@ -230,7 +277,11 @@ export class ReportsComponent implements OnInit {
|
||||
} else {
|
||||
if (this.selectedSubCategory) {
|
||||
// keep showing sub-cat table
|
||||
} else if (this.selectedCategory && SUB_CATEGORY_RULES[this.selectedCategory] && this.filteredReports.length > 15) {
|
||||
} else if (
|
||||
this.selectedCategory &&
|
||||
SUB_CATEGORY_RULES[this.selectedCategory] &&
|
||||
this.filteredReports.length > 15
|
||||
) {
|
||||
this.isSearching = false;
|
||||
this.showSubCategories = true;
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
<!--
|
||||
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">
|
||||
<div class="page-header">
|
||||
<fa-icon icon="shield-alt" class="header-icon"></fa-icon>
|
||||
@@ -12,11 +20,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<div class="filter-chips">
|
||||
<button
|
||||
mat-stroked-button
|
||||
[class.active-filter]="activeFilter === null"
|
||||
(click)="filterByResult(null)"
|
||||
>
|
||||
<button mat-stroked-button [class.active-filter]="activeFilter === null" (click)="filterByResult(null)">
|
||||
<fa-icon icon="list" class="chip-icon"></fa-icon> All
|
||||
</button>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user