feat: add RBI circulars, CKYC, KYC, Superset analytics, passbook modules and UI updates

Add new feature modules (RBI feeds, CKYC, KYC tab, Form60, Superset analytics, passbook),
update client family members, dashboard, login page, sidenav, images, translations,
environment config, and fix RBI feeds API auth by using relative URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-10 15:13:35 +05:30
co-authored by Claude Opus 4.6
parent 07f701f654
commit 73aa69ee02
84 changed files with 6256 additions and 700 deletions
@@ -0,0 +1,151 @@
<!--
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="passbook-container">
<!-- Date Range Filter -->
<div class="passbook-filter-bar">
<form [formGroup]="dateRangeForm" class="passbook-filter-form">
<mat-form-field appearance="outline" class="passbook-date-field" (click)="fromDatePicker.open()">
<mat-label>{{ 'labels.inputs.From Date' | translate }}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="fromDatePicker" formControlName="fromDate" />
<mat-datepicker-toggle matSuffix [for]="fromDatePicker"></mat-datepicker-toggle>
<mat-datepicker #fromDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="passbook-date-field" (click)="toDatePicker.open()">
<mat-label>{{ 'labels.inputs.To Date' | translate }}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="toDatePicker" formControlName="toDate" />
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
<mat-datepicker #toDatePicker></mat-datepicker>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="!dateRangeForm.valid"
(click)="filterTransactions()"
>
<mat-icon>filter_list</mat-icon>
{{ 'labels.buttons.Filter' | translate }}
</button>
@if (isFiltered) {
<button mat-stroked-button type="button" (click)="clearFilter()">
<mat-icon>clear</mat-icon>
{{ 'labels.buttons.Clear' | translate }}
</button>
}
<span class="passbook-filter-spacer"></span>
<button mat-raised-button color="accent" type="button" (click)="printPassbook()">
<mat-icon>print</mat-icon>
{{ 'labels.buttons.Print Passbook' | translate }}
</button>
</form>
</div>
<!-- Passbook Preview (Printable Content) -->
<div class="passbook-preview" #passbookContent>
<div class="passbook-header">
<h2>{{ 'labels.heading.Savings Account Passbook' | translate }}</h2>
<h3>{{ productName }}</h3>
</div>
<!-- Account Info -->
<div class="passbook-info-grid">
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Account Number' | translate }}</div>
<div class="value">{{ accountNo }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Account Holder' | translate }}</div>
<div class="value">{{ clientName }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Savings Product' | translate }}</div>
<div class="value">{{ productName }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Opening Date' | translate }}</div>
<div class="value">{{ activatedOnDate | dateFormat }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Status' | translate }}</div>
<div class="value">{{ status }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Interest Rate' | translate }}</div>
<div class="value">{{ interestRate }}%</div>
</div>
</div>
<!-- Balance Summary -->
<div class="passbook-balance-row">
<div class="balance-item">
<span>{{ 'labels.inputs.Current Balance' | translate }}: </span>
<b>{{ accountBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</b>
</div>
<div class="balance-item">
<span>{{ 'labels.inputs.Available Balance' | translate }}: </span>
<b>{{ availableBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</b>
</div>
</div>
<!-- Transactions Table -->
@if (filteredTransactions.length > 0) {
<table>
<thead>
<tr>
<th>#</th>
<th>{{ 'labels.inputs.Date' | translate }}</th>
<th>{{ 'labels.inputs.Description' | translate }}</th>
<th class="text-right">{{ 'labels.inputs.Debit' | translate }}</th>
<th class="text-right">{{ 'labels.inputs.Credit' | translate }}</th>
<th class="text-right">{{ 'labels.inputs.Balance' | translate }}</th>
</tr>
</thead>
<tbody>
@for (txn of filteredTransactions; track txn.id; let i = $index) {
<tr [class.reversed]="txn.reversed">
<td>{{ i + 1 }}</td>
<td>{{ txn.date | dateFormat }}</td>
<td>{{ txn.transactionType.value }}</td>
<td class="text-right">
@if (isDebit(txn.transactionType)) {
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
}
</td>
<td class="text-right">
@if (!isDebit(txn.transactionType)) {
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
}
</td>
<td class="text-right">
{{ txn.runningBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
</tbody>
</table>
} @else {
<div class="passbook-no-data">
<mat-icon>info_outline</mat-icon>
<span>{{ 'labels.text.No transactions found for the selected date range.' | translate }}</span>
</div>
}
<!-- Footer -->
<div class="passbook-footer">
{{ 'labels.text.Generated on' | translate }}: {{ maxDate | dateFormat }}
&nbsp;|&nbsp;
{{ 'labels.text.Total Transactions' | translate }}: {{ filteredTransactions.length }}
</div>
</div>
</div>
@@ -0,0 +1,206 @@
/**
* 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/.
*/
.passbook-container {
padding: 16px;
}
// Filter Bar
.passbook-filter-bar {
margin-bottom: 16px;
}
.passbook-filter-form {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.passbook-date-field {
width: 180px;
}
.passbook-filter-spacer {
flex: 1;
}
// Passbook Preview
.passbook-preview {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 24px;
background: #fff;
}
.passbook-header {
text-align: center;
margin-bottom: 20px;
border-bottom: 2px solid #1976d2;
padding-bottom: 12px;
h2 {
color: #1976d2;
font-size: 20px;
margin: 0 0 4px;
font-weight: 600;
}
h3 {
font-size: 14px;
font-weight: 400;
color: #666;
margin: 0;
}
}
.passbook-info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 20px;
margin-bottom: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
}
.passbook-info-item {
.label {
font-size: 10px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
font-size: 13px;
color: #333;
margin-top: 2px;
}
}
.passbook-balance-row {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
padding: 10px 14px;
background: #e3f2fd;
border-radius: 6px;
font-size: 14px;
.balance-item {
b {
color: #1976d2;
}
}
}
// Transactions Table
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th {
background: #1976d2;
color: #fff;
padding: 8px 10px;
text-align: left;
font-weight: 600;
font-size: 12px;
}
td {
padding: 6px 10px;
border-bottom: 1px solid #e0e0e0;
}
tr:nth-child(even) {
background: #fafafa;
}
tr:hover {
background: #f0f0f0;
}
.reversed {
text-decoration: line-through;
color: #999;
}
}
.text-right {
text-align: right;
}
.passbook-no-data {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 32px;
color: #888;
font-size: 14px;
mat-icon {
color: #bbb;
}
}
.passbook-footer {
margin-top: 16px;
font-size: 11px;
color: #999;
text-align: center;
border-top: 1px solid #e0e0e0;
padding-top: 10px;
}
// Responsive
@media (width <= 768px) {
.passbook-info-grid {
grid-template-columns: 1fr 1fr;
}
.passbook-balance-row {
flex-direction: column;
gap: 8px;
}
.passbook-filter-form {
flex-direction: column;
align-items: stretch;
}
.passbook-date-field {
width: 100%;
}
.passbook-filter-spacer {
display: none;
}
}
@media (width <= 480px) {
.passbook-info-grid {
grid-template-columns: 1fr;
}
}
// Print-specific styles
@media print {
.passbook-filter-bar {
display: none !important;
}
.passbook-preview {
border: none;
padding: 0;
}
}
@@ -0,0 +1,186 @@
/**
* 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, OnDestroy, inject, ElementRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { CurrencyPipe } from '@angular/common';
import { Subject, takeUntil } from 'rxjs';
/** Custom Services */
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { Currency } from 'app/shared/models/general.model';
import {
SavingsAccountTransaction,
SavingsAccountTransactionType
} from 'app/savings/models/savings-account-transaction.model';
import { MatIcon } from '@angular/material/icon';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-passbook-tab',
templateUrl: './passbook-tab.component.html',
styleUrls: ['./passbook-tab.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatIcon,
FaIconComponent,
DateFormatPipe,
CurrencyPipe
]
})
export class PassbookTabComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute);
private formBuilder = inject(UntypedFormBuilder);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private destroy$ = new Subject<void>();
@ViewChild('passbookContent') passbookContent: ElementRef;
/** Account data */
accountNo: string = '';
clientName: string = '';
productName: string = '';
currency: Currency | null = null;
status: string = '';
interestRate: number = 0;
accountBalance: number = 0;
availableBalance: number = 0;
activatedOnDate: any = null;
/** Transactions */
allTransactions: SavingsAccountTransaction[] = [];
filteredTransactions: SavingsAccountTransaction[] = [];
/** Date range form */
minDate = new Date(2000, 0, 1);
maxDate = new Date();
dateRangeForm: any;
isFiltered = false;
ngOnInit(): void {
this.maxDate = this.settingsService.businessDate;
this.route.parent.data.pipe(takeUntil(this.destroy$)).subscribe((data: { savingsAccountData: any }) => {
const account = data.savingsAccountData;
this.accountNo = account.accountNo || '';
this.clientName = account.clientName || account.groupName || '';
this.productName = account.savingsProductName || '';
this.currency = account.currency || null;
this.status = account.status?.value || '';
this.interestRate = account.nominalAnnualInterestRate || 0;
this.accountBalance = account.summary?.accountBalance || 0;
this.availableBalance = account.summary?.availableBalance || 0;
this.activatedOnDate = account.timeline?.activatedOnDate || null;
this.allTransactions = account.transactions || [];
this.filteredTransactions = [...this.allTransactions];
});
this.dateRangeForm = this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
]
});
}
isDebit(transactionType: SavingsAccountTransactionType): boolean {
return (
transactionType.withdrawal === true ||
transactionType.feeDeduction === true ||
transactionType.overdraftInterest === true ||
transactionType.withholdTax === true
);
}
filterTransactions(): void {
const fromDate = this.dateRangeForm.value.fromDate;
const toDate = this.dateRangeForm.value.toDate;
if (!fromDate || !toDate) return;
const from = new Date(fromDate).getTime();
const to = new Date(toDate).getTime();
this.filteredTransactions = this.allTransactions.filter((txn: SavingsAccountTransaction) => {
const txnDate = this.getTransactionDateAsTime(txn.date);
return txnDate >= from && txnDate <= to;
});
this.isFiltered = true;
}
clearFilter(): void {
this.dateRangeForm.reset();
this.filteredTransactions = [...this.allTransactions];
this.isFiltered = false;
}
private getTransactionDateAsTime(date: any): number {
if (Array.isArray(date) && date.length >= 3) {
return new Date(date[0], date[1] - 1, date[2]).getTime();
}
return new Date(date).getTime();
}
printPassbook(): void {
const content = this.passbookContent?.nativeElement?.innerHTML;
if (!content) return;
const printWindow = window.open('', 'Passbook', 'height=700,width=900');
printWindow.document.write(`
<html>
<head>
<title>Savings Account Passbook - ${this.accountNo}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; padding: 20px; color: #333; }
.passbook-header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #1976d2; padding-bottom: 12px; }
.passbook-header h2 { color: #1976d2; font-size: 18px; margin-bottom: 4px; }
.passbook-header h3 { font-size: 14px; font-weight: 400; color: #666; }
.passbook-info-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px 16px; margin-bottom: 16px; padding: 10px; background: #f5f5f5; border-radius: 4px; }
.passbook-info-item { font-size: 12px; }
.passbook-info-item .label { font-weight: 600; color: #666; text-transform: uppercase; font-size: 10px; }
.passbook-info-item .value { color: #333; }
.passbook-balance-row { display: flex; justify-content: space-between; margin-bottom: 16px; padding: 8px 10px; background: #e3f2fd; border-radius: 4px; font-size: 13px; }
.passbook-balance-row .balance-item b { color: #1976d2; }
table { width: 100%; border-collapse: collapse; font-size: 11px; }
th { background: #1976d2; color: #fff; padding: 6px 8px; text-align: left; font-weight: 600; }
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; }
tr:nth-child(even) { background: #fafafa; }
.text-right { text-align: right; }
.passbook-footer { margin-top: 16px; font-size: 10px; color: #999; text-align: center; border-top: 1px solid #e0e0e0; padding-top: 8px; }
.reversed { text-decoration: line-through; color: #999; }
</style>
</head>
<body>
${content}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
printWindow.close();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
@@ -163,6 +163,15 @@
>
{{ 'labels.inputs.Transactions' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./passbook']"
routerLinkActive
#passbook="routerLinkActive"
[active]="passbook.isActive"
>
{{ 'labels.inputs.Passbook' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./charges']"
@@ -51,6 +51,7 @@ import { SavingNotesResolver } from './common-resolvers/saving-notes.resolver';
import { SavingDocumentsResolver } from './common-resolvers/saving-documents.resolver';
import { SavingsTransactionGeneralTabComponent } from './savings-account-view/transactions/view-transaction/savings-transaction-general-tab/savings-transaction-general-tab.component';
import { GeneralTabComponent } from './savings-account-view/general-tab/general-tab.component';
import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbook-tab.component';
/** Savings Routes */
const routes: Routes = [
@@ -104,6 +105,11 @@ const routes: Routes = [
}
]
},
{
path: 'passbook',
component: PassbookTabComponent,
data: { title: 'Passbook', breadcrumb: 'Passbook', routeParamBreadcrumb: false }
},
{
path: 'charges',
component: ChargesTabComponent,
+3 -1
View File
@@ -62,6 +62,7 @@ import { SavingsTransactionGeneralTabComponent } from './savings-account-view/tr
import { SavingsTransactionDatatableTabComponent } from './savings-account-view/transactions/view-transaction/savings-transaction-datatable-tab/savings-transaction-datatable-tab.component';
import { GeneralTabComponent } from './savings-account-view/general-tab/general-tab.component';
import { SavingsActiveClientMembersComponent } from './savings-account-stepper/savings-active-client-members/savings-active-client-members.component';
import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbook-tab.component';
/**
* Savings Module
@@ -119,7 +120,8 @@ import { SavingsActiveClientMembersComponent } from './savings-account-stepper/s
SavingsTransactionGeneralTabComponent,
SavingsTransactionDatatableTabComponent,
GeneralTabComponent,
SavingsActiveClientMembersComponent
SavingsActiveClientMembersComponent,
PassbookTabComponent
],
providers: []
})