diff --git a/src/app/loans/loans-routing.module.ts b/src/app/loans/loans-routing.module.ts
index 4a551deb5..23dc4b007 100644
--- a/src/app/loans/loans-routing.module.ts
+++ b/src/app/loans/loans-routing.module.ts
@@ -37,6 +37,7 @@ import { ExportTransactionsComponent } from './loans-view/transactions/export-tr
import { GlimAccountComponent } from './glim-account/glim-account.component';
import { CreateGlimAccountComponent } from './glim-account/create-glim-account/create-glim-account.component';
import { LoanBuyDownFeesTabComponent } from './loans-view/loan-buy-down-fees-tab/loan-buy-down-fees-tab.component';
+import { LoanAccountDashboardComponent } from './loans-view/loan-account-dashboard/loan-account-dashboard.component';
/** Custom Resolvers */
import { LoanDetailsResolver } from './common-resolvers/loan-details.resolver';
@@ -112,6 +113,11 @@ const routes: Routes = [
data: { title: 'General', breadcrumb: 'General', routeParamBreadcrumb: false },
resolve: {}
},
+ {
+ path: 'dashboard',
+ component: LoanAccountDashboardComponent,
+ data: { title: 'Dashboard', breadcrumb: 'Dashboard', routeParamBreadcrumb: false }
+ },
{
path: 'accountdetail',
component: AccountDetailsComponent,
diff --git a/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.html b/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.html
new file mode 100644
index 000000000..fa016797d
--- /dev/null
+++ b/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+ {{ 'labels.inputs.Principal Amount' | translate }}
+ {{ principalAmount | number: '1.2-2' }}
+
+
+
+
+
+ {{ 'labels.inputs.Total Repaid' | translate }}
+ {{ totalRepaid | number: '1.2-2' }}
+
+
+
+
+
+
+ {{ 'labels.inputs.Outstanding Balance' | translate }}
+ {{ outstandingBalance | number: '1.2-2' }}
+
+
+
+
+
+ {{ 'labels.inputs.Interest Charged' | translate }}
+ {{ interestCharged | number: '1.2-2' }}
+
+
+
+
+
+
+
+
+
+ {{ 'labels.heading.Repayment Progress' | translate }}
+
+
+
+
+
+ @if (!loanData) {
+
{{ 'labels.text.Loading data' | translate }}...
+ }
+
+
+
+
+
+
+
+ {{ 'labels.heading.Payment Schedule' | translate }}
+
+
+
+
+
+ @if (!loanData) {
+
{{ 'labels.text.Loading data' | translate }}...
+ } @else if (!loanData.repaymentSchedule?.periods || loanData.repaymentSchedule.periods.length === 0) {
+
+ {{ 'labels.text.No repayment schedule available' | translate }}
+
+ }
+
+
+
+
+
diff --git a/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.scss b/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.scss
new file mode 100644
index 000000000..97d4c3c7d
--- /dev/null
+++ b/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.scss
@@ -0,0 +1,189 @@
+/**
+ * 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/.
+ */
+
+.dashboard-container {
+ padding: 20px;
+ background: linear-gradient(135deg, #0d47a1 0%, #1565c0 50%, #1976d2 100%);
+}
+
+.dashboard-header-card {
+ margin-bottom: 24px;
+ padding: 0;
+ border: none;
+ background: rgb(255 255 255 / 10%);
+ backdrop-filter: blur(10px);
+
+ .header {
+ padding: 16px 24px;
+ background: transparent;
+ border-bottom: 1px solid rgb(255 255 255 / 20%);
+
+ h3 {
+ margin: 0;
+ color: white;
+ font-size: 22px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+
+ fa-icon {
+ color: white;
+ }
+ }
+ }
+}
+
+.metrics-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ margin-bottom: 24px;
+}
+
+.metrics-grid .metric-card {
+ flex: 1 1 250px;
+ min-width: 250px;
+}
+
+.metric-card {
+ background: rgb(255 255 255 / 95%);
+ border: none;
+ border-radius: 16px;
+ box-shadow: 0 8px 24px rgb(0 0 0 / 15%);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 32px rgb(0 0 0 / 20%);
+ }
+
+ mat-card-content {
+ padding: 24px;
+ }
+
+ .metric-label {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 8px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-weight: 600;
+ }
+
+ .metric-value {
+ font-size: 32px;
+ font-weight: 700;
+ color: #1976d2;
+ margin-bottom: 12px;
+
+ &.success {
+ color: #4caf50;
+ }
+
+ &.warning {
+ color: #ff9800;
+ }
+ }
+
+ .metric-progress {
+ margin-top: 12px;
+ height: 8px;
+ background: linear-gradient(90deg, #e3f2fd 0%, #bbdefb 100%);
+ border-radius: 4px;
+ overflow: hidden;
+
+ .progress-bar {
+ height: 100%;
+ background: linear-gradient(90deg, #4caf50 0%, #66bb6a 100%);
+ transition: width 0.6s ease;
+ box-shadow: 0 2px 8px rgb(76 175 80 / 30%);
+ }
+ }
+}
+
+.charts-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+}
+
+.charts-grid .chart-card {
+ flex: 1 1 450px;
+ min-width: 450px;
+}
+
+.chart-card {
+ background: rgb(255 255 255 / 95%);
+ border: none;
+ border-radius: 16px;
+ box-shadow: 0 8px 24px rgb(0 0 0 / 15%);
+ overflow: hidden;
+
+ mat-card-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid #e0e0e0;
+ background: linear-gradient(135deg, #f5f5f5 0%, #fafafa 100%);
+
+ mat-card-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1976d2;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 0;
+
+ fa-icon {
+ color: #1976d2;
+ }
+ }
+ }
+
+ mat-card-content {
+ padding: 24px;
+ background: white;
+ }
+
+ .chart-container {
+ width: 100%;
+ height: 350px;
+ position: relative;
+
+ canvas {
+ max-width: 100%;
+ height: auto !important;
+ }
+
+ .no-data-message {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: #999;
+ font-size: 14px;
+ text-align: center;
+ font-weight: 500;
+ }
+ }
+}
+
+@media (width <= 768px) {
+ .metrics-grid .metric-card {
+ flex: 1 1 100%;
+ min-width: 100%;
+ }
+
+ .charts-grid .chart-card {
+ flex: 1 1 100%;
+ min-width: 100%;
+ }
+
+ .dashboard-container {
+ padding: 12px;
+ }
+}
diff --git a/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.ts b/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.ts
new file mode 100644
index 000000000..0618838d8
--- /dev/null
+++ b/src/app/loans/loans-view/loan-account-dashboard/loan-account-dashboard.component.ts
@@ -0,0 +1,335 @@
+/**
+ * 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, AfterViewInit, ViewChild, ElementRef, inject, OnDestroy } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { Subscription } from 'rxjs';
+import { MatCard, MatCardHeader, MatCardContent, MatCardTitle } from '@angular/material/card';
+import { Chart, registerables } from 'chart.js';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+
+Chart.register(...registerables);
+
+/**
+ * Loan Account Dashboard Component
+ * Displays graphical analysis and metrics for a specific loan account
+ */
+@Component({
+ selector: 'mifosx-loan-account-dashboard',
+ standalone: true,
+ templateUrl: './loan-account-dashboard.component.html',
+ styleUrls: ['./loan-account-dashboard.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ MatCard,
+ MatCardHeader,
+ MatCardContent,
+ MatCardTitle
+ ]
+})
+export class LoanAccountDashboardComponent implements OnInit, AfterViewInit, OnDestroy {
+ private route = inject(ActivatedRoute);
+ private translate = inject(TranslateService);
+ private langChangeSubscription?: Subscription;
+ private routeDataSubscription?: Subscription;
+
+ @ViewChild('statusChart', { static: false }) statusChartCanvas!: ElementRef;
+ @ViewChild('paymentsChart', { static: false }) paymentsChartCanvas!: ElementRef;
+
+ private statusChart: any;
+ private paymentsChart: any;
+ private initTimeout: number | null = null;
+
+ /** Loan data */
+ loanData: any;
+ loanId: string = '';
+
+ /** Metrics */
+ principalAmount: number = 0;
+ totalRepaid: number = 0;
+ outstandingBalance: number = 0;
+ interestCharged: number = 0;
+ totalExpected: number = 0;
+ progressPercentage: number = 0;
+
+ ngOnInit(): void {
+ this.loanId = this.route.parent?.snapshot.paramMap.get('loanId') || '';
+
+ this.routeDataSubscription = this.route.parent!.data.subscribe((data: { loanDetailsData: any }) => {
+ if (data.loanDetailsData) {
+ this.loanData = data.loanDetailsData;
+ this.calculateMetrics();
+ this.initTimeout = window.setTimeout(() => {
+ this.createStatusChart();
+ this.createPaymentsChart();
+ }, 100);
+ }
+ });
+
+ this.langChangeSubscription = this.translate.onLangChange.subscribe(() => {
+ if (this.statusChart) {
+ this.createStatusChart();
+ }
+ if (this.paymentsChart) {
+ this.createPaymentsChart();
+ }
+ });
+ }
+
+ ngAfterViewInit(): void {
+ setTimeout(() => {
+ this.createStatusChart();
+ this.createPaymentsChart();
+ }, 100);
+ }
+
+ calculateMetrics(): void {
+ if (!this.loanData) return;
+
+ this.principalAmount = this.loanData.principal || 0;
+ this.totalRepaid = this.loanData.summary?.totalRepayment || 0;
+ this.outstandingBalance = this.loanData.summary?.totalOutstanding || 0;
+ this.interestCharged = this.loanData.summary?.interestCharged || 0;
+ this.totalExpected = this.loanData.summary?.totalExpectedRepayment || 0;
+
+ if (this.totalExpected === 0) {
+ this.progressPercentage = 0;
+ } else {
+ this.progressPercentage = Math.min(100, Math.max(0, (this.totalRepaid / this.totalExpected) * 100));
+ }
+ }
+
+ createStatusChart(): void {
+ if (!this.statusChartCanvas) return;
+
+ if (this.statusChart) {
+ this.statusChart.destroy();
+ }
+
+ const canvas = this.statusChartCanvas.nativeElement;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ const repaidPercentage = Math.min(
+ 100,
+ Math.max(0, this.totalExpected > 0 ? (this.totalRepaid / this.totalExpected) * 100 : 0)
+ );
+ const outstandingPercentage = Math.max(0, 100 - repaidPercentage);
+
+ this.statusChart = new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: [
+ this.translate.instant('labels.inputs.Total Repaid'),
+ this.translate.instant('labels.inputs.Outstanding Balance')
+ ],
+ datasets: [
+ {
+ data: [
+ repaidPercentage,
+ outstandingPercentage
+ ],
+ backgroundColor: [
+ '#4CAF50',
+ '#FF9800'
+ ],
+ borderWidth: 0,
+ borderColor: 'transparent',
+ hoverBorderWidth: 3,
+ hoverBorderColor: '#fff'
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ aspectRatio: 1.5,
+ cutout: '70%',
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ padding: 20,
+ font: {
+ size: 13,
+ weight: '600'
+ },
+ usePointStyle: true,
+ pointStyle: 'circle',
+ generateLabels: (chart: any) => {
+ const data = chart.data;
+ if (data.labels.length && data.datasets.length) {
+ return data.labels.map((label: string, i: number) => {
+ const value = data.datasets[0].data[i];
+ return {
+ text: `${label}: ${value.toFixed(1)}%`,
+ fillStyle: data.datasets[0].backgroundColor[i],
+ hidden: false,
+ index: i
+ };
+ });
+ }
+ return [];
+ }
+ }
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ padding: 12,
+ titleFont: {
+ size: 14,
+ weight: 'bold'
+ },
+ bodyFont: {
+ size: 13
+ },
+ callbacks: {
+ label: function (context: any) {
+ const label = context.label || '';
+ const value = context.parsed || 0;
+ return `${label}: ${value.toFixed(1)}%`;
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createPaymentsChart(): void {
+ if (!this.paymentsChartCanvas) return;
+
+ if (this.paymentsChart) {
+ this.paymentsChart.destroy();
+ }
+
+ const canvas = this.paymentsChartCanvas.nativeElement;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ const repaymentSchedule = this.loanData?.repaymentSchedule?.periods || [];
+ const labels: string[] = [];
+ const principalData: number[] = [];
+ const interestData: number[] = [];
+
+ repaymentSchedule.forEach((period: any) => {
+ if (period.period && period.period > 0) {
+ labels.push(`${this.translate.instant('labels.inputs.Period')} ${period.period}`);
+ principalData.push(period.principalDue || 0);
+ interestData.push(period.interestDue || 0);
+ }
+ });
+
+ this.paymentsChart = new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: labels.slice(0, 10),
+ datasets: [
+ {
+ label: this.translate.instant('labels.inputs.Principal'),
+ data: principalData.slice(0, 10),
+ backgroundColor: '#2196F3',
+ borderWidth: 0,
+ borderRadius: 8,
+ barThickness: 24
+ },
+ {
+ label: this.translate.instant('labels.inputs.Interest'),
+ data: interestData.slice(0, 10),
+ backgroundColor: '#FFC107',
+ borderWidth: 0,
+ borderRadius: 8,
+ barThickness: 24
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ aspectRatio: 2,
+ plugins: {
+ legend: {
+ position: 'top',
+ labels: {
+ usePointStyle: true,
+ pointStyle: 'circle',
+ padding: 15,
+ font: {
+ size: 13,
+ weight: '600'
+ }
+ }
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ padding: 12,
+ titleFont: {
+ size: 14,
+ weight: 'bold'
+ },
+ bodyFont: {
+ size: 13
+ },
+ callbacks: {
+ label: function (context: any) {
+ const label = context.dataset.label || '';
+ const value = context.parsed.y || 0;
+ return `${label}: ${value.toLocaleString()}`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ stacked: true,
+ grid: {
+ display: false
+ },
+ ticks: {
+ font: {
+ size: 11
+ }
+ }
+ },
+ y: {
+ stacked: true,
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(0, 0, 0, 0.05)'
+ },
+ ticks: {
+ font: {
+ size: 11
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ ngOnDestroy(): void {
+ if (this.initTimeout !== null) {
+ clearTimeout(this.initTimeout);
+ this.initTimeout = null;
+ }
+ if (this.routeDataSubscription) {
+ this.routeDataSubscription.unsubscribe();
+ }
+ if (this.langChangeSubscription) {
+ this.langChangeSubscription.unsubscribe();
+ }
+ if (this.statusChart) {
+ this.statusChart.destroy();
+ }
+ if (this.paymentsChart) {
+ this.paymentsChart.destroy();
+ }
+ }
+}
diff --git a/src/app/loans/loans-view/loans-view.component.html b/src/app/loans/loans-view/loans-view.component.html
index 108197f72..16b9ba92f 100644
--- a/src/app/loans/loans-view/loans-view.component.html
+++ b/src/app/loans/loans-view/loans-view.component.html
@@ -205,6 +205,15 @@
>
{{ 'labels.inputs.General' | translate }}
+
+ {{ 'labels.inputs.Dashboard' | translate }}
+