- KYC tab: fix Verify (PAN) and Send OTP (Aadhaar) being disabled in edit mode; unmask Aadhaar input and show on-file masked value as hint; resolve kycId from id/kycId/resourceId fallbacks. - Client stepper/view: add familyAllowed / nomineeAllowed predicates + CanActivate route guards. Family tab hidden for non-individuals, Nominee tab hidden for non-proprietor entities. Family-members step accepts a nomineeOnly input for proprietorship clients. Add/Edit/Delete buttons inside family-members and nominee tabs gated by canAdd. - cfcfrms: add cfcfrms-shell wrapper, restyle complaint/fraud/mule/escalate pages and dashboard. - Savings: identity-verify and transaction-confirm dialogs, manage / activate / add-charge flows, account preview styling. - Tasks, core shell, account-nominees, icons: minor style and behaviour updates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
170 lines
5.0 KiB
TypeScript
170 lines
5.0 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/.
|
|
*/
|
|
|
|
import { Component, OnInit, inject, ViewChild, ElementRef } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { RouterModule } from '@angular/router';
|
|
import { MatTableModule } from '@angular/material/table';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { Chart, registerables } from 'chart.js';
|
|
import { TranslateModule } from '@ngx-translate/core';
|
|
import { CfcfrmsService } from '../cfcfrms.service';
|
|
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
|
|
import { Complaint, DashboardStats } from '../models/cfcfrms.models';
|
|
|
|
Chart.register(...registerables);
|
|
|
|
@Component({
|
|
selector: 'mifosx-cfcfrms-dashboard',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
RouterModule,
|
|
MatTableModule,
|
|
MatButtonModule,
|
|
MatProgressSpinnerModule,
|
|
FaIconComponent,
|
|
TranslateModule,
|
|
StatusBadgeComponent
|
|
],
|
|
templateUrl: './cfcfrms-dashboard.component.html',
|
|
styleUrls: ['./cfcfrms-dashboard.component.scss']
|
|
})
|
|
export class CfcfrmsDashboardComponent implements OnInit {
|
|
private cfcfrmsService = inject(CfcfrmsService);
|
|
|
|
stats: DashboardStats;
|
|
recentComplaints: Complaint[] = [];
|
|
isLoading = true;
|
|
displayedColumns = [
|
|
'ticketId',
|
|
'complainantName',
|
|
'fraudAmount',
|
|
'status',
|
|
'createdAt',
|
|
'actions'
|
|
];
|
|
|
|
@ViewChild('donutCanvas') donutCanvas: ElementRef<HTMLCanvasElement>;
|
|
@ViewChild('barCanvas') barCanvas: ElementRef<HTMLCanvasElement>;
|
|
|
|
private donutChart: any;
|
|
private barChart: any;
|
|
|
|
ngOnInit(): void {
|
|
this.loadData();
|
|
}
|
|
|
|
loadData(): void {
|
|
this.isLoading = true;
|
|
this.cfcfrmsService.getStats().subscribe({
|
|
next: (stats) => {
|
|
this.stats = stats;
|
|
setTimeout(() => this.renderCharts(), 100);
|
|
},
|
|
error: () => {
|
|
this.stats = {
|
|
totalComplaints: 0,
|
|
pendingReview: 0,
|
|
lienMarked: 0,
|
|
fundsRecovered: 0,
|
|
statusDistribution: {},
|
|
amountByStatus: {}
|
|
};
|
|
}
|
|
});
|
|
|
|
this.cfcfrmsService.getComplaints({ page: 0, size: 5 }).subscribe({
|
|
next: (res) => {
|
|
this.recentComplaints = res.content;
|
|
this.isLoading = false;
|
|
},
|
|
error: () => {
|
|
this.recentComplaints = [];
|
|
this.isLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private renderCharts(): void {
|
|
if (!this.stats) return;
|
|
|
|
const statusColors: Record<string, string> = {
|
|
RECEIVED: '#1E88E5',
|
|
UNDER_REVIEW: '#F59E0B',
|
|
LIEN_MARKED: '#EAB308',
|
|
DEBIT_FROZEN: '#EF4444',
|
|
FUNDS_MOVED: '#8B5CF6',
|
|
RESOLVED: '#10B981',
|
|
REJECTED: '#6B7280'
|
|
};
|
|
|
|
// Donut chart
|
|
if (this.donutCanvas) {
|
|
const dist = this.stats.statusDistribution || {};
|
|
const labels = Object.keys(dist);
|
|
const data = Object.values(dist);
|
|
const colors = labels.map((l) => statusColors[l] || '#999');
|
|
|
|
if (this.donutChart) this.donutChart.destroy();
|
|
this.donutChart = new Chart(this.donutCanvas.nativeElement, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: labels.map((l) => l.replace('_', ' ')),
|
|
datasets: [{ data, backgroundColor: colors, borderWidth: 2, borderColor: '#fff' }]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'bottom', labels: { padding: 16, font: { size: 12 } } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bar chart
|
|
if (this.barCanvas) {
|
|
const amt = this.stats.amountByStatus || {};
|
|
const labels = Object.keys(amt);
|
|
const data = Object.values(amt);
|
|
const colors = labels.map((l) => statusColors[l] || '#999');
|
|
|
|
if (this.barChart) this.barChart.destroy();
|
|
this.barChart = new Chart(this.barCanvas.nativeElement, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels.map((l) => l.replace('_', ' ')),
|
|
datasets: [{ label: 'Fraud Amount (₹)', data, backgroundColor: colors, borderRadius: 4 }]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { beginAtZero: true, ticks: { callback: (v: any) => '₹' + (v / 1000).toFixed(0) + 'K' } },
|
|
x: { ticks: { font: { size: 11 } } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
formatCurrency(amount: number): string {
|
|
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(
|
|
amount
|
|
);
|
|
}
|
|
|
|
formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
|
}
|
|
}
|