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,185 @@
<!--
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="form60-container">
<!-- Warning Notice -->
<div class="form60-notice">
<mat-icon>warning</mat-icon>
<span>{{ 'labels.text.PAN not provided. Please fill Form 60 as per Income Tax Rule 114B.' | translate }}</span>
</div>
@if (loading) {
<div class="form60-loading">
<mat-spinner diameter="40"></mat-spinner>
</div>
} @else {
<!-- Read-only view when data exists and not editing -->
@if (form60Data && !isEditMode) {
<mat-card>
<mat-card-header>
<mat-card-title>{{ 'labels.inputs.Form 60 Declaration' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="form60-summary">
@if (form60Data.fullName) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Full Name' | translate }}</span>
<span class="value">{{ form60Data.fullName }}</span>
</div>
}
@if (form60Data.dateOfBirth) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Date Of Birth' | translate }}</span>
<span class="value">{{ form60Data.dateOfBirth | dateFormat }}</span>
</div>
}
@if (form60Data.fatherName) {
<div class="form60-field">
<span class="label">{{ "labels.inputs.Father's Name" | translate }}</span>
<span class="value">{{ form60Data.fatherName }}</span>
</div>
}
@if (form60Data.address) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Address' | translate }}</span>
<span class="value">{{ form60Data.address }}</span>
</div>
}
@if (form60Data.transactionType) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Transaction Type' | translate }}</span>
<span class="value">{{ form60Data.transactionType }}</span>
</div>
}
@if (form60Data.transactionAmount) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Transaction Amount' | translate }}</span>
<span class="value">{{ form60Data.transactionAmount }}</span>
</div>
}
@if (form60Data.estimatedTotalIncome) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Estimated Total Income' | translate }}</span>
<span class="value">{{ form60Data.estimatedTotalIncome }}</span>
</div>
}
@if (form60Data.reasonForNoPan) {
<div class="form60-field">
<span class="label">{{ 'labels.inputs.Reason for Not Having PAN' | translate }}</span>
<span class="value">{{ form60Data.reasonForNoPan }}</span>
</div>
}
</div>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="toggleEditMode()">
<mat-icon>edit</mat-icon>
{{ 'labels.buttons.Edit' | translate }}
</button>
</mat-card-actions>
</mat-card>
}
<!-- Edit / Create Form -->
@if (isEditMode) {
<mat-card>
<mat-card-header>
<mat-card-title>{{ 'labels.inputs.Form 60 Declaration' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form60Form" (ngSubmit)="submit()">
<div class="layout-row-wrap responsive-column align-start-center">
<!-- Full Name -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ 'labels.inputs.Full Name' | translate }}</mat-label>
<input formControlName="fullName" required matInput />
@if (form60Form.controls.fullName.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Full Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<!-- Date of Birth -->
<mat-form-field class="flex-50-minus-25px" (click)="dobPicker.open()">
<mat-label>{{ 'labels.inputs.Date Of Birth' | translate }}</mat-label>
<input formControlName="dateOfBirth" matInput [max]="maxDate" [matDatepicker]="dobPicker" />
<mat-datepicker-toggle matSuffix [for]="dobPicker"></mat-datepicker-toggle>
<mat-datepicker #dobPicker></mat-datepicker>
</mat-form-field>
<!-- Father's Name -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ "labels.inputs.Father's Name" | translate }}</mat-label>
<input formControlName="fatherName" matInput />
</mat-form-field>
<!-- Address -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ 'labels.inputs.Address' | translate }}</mat-label>
<input formControlName="address" matInput />
</mat-form-field>
<!-- Transaction Type -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ 'labels.inputs.Transaction Type' | translate }}</mat-label>
<mat-select formControlName="transactionType">
@for (txnType of transactionTypes; track txnType.value) {
<mat-option [value]="txnType.value">{{ txnType.key | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<!-- Transaction Amount -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ 'labels.inputs.Transaction Amount' | translate }}</mat-label>
<input type="number" formControlName="transactionAmount" matInput />
</mat-form-field>
<!-- Estimated Total Income -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ 'labels.inputs.Estimated Total Income' | translate }}</mat-label>
<input type="number" formControlName="estimatedTotalIncome" matInput />
</mat-form-field>
<!-- Reason for Not Having PAN -->
<mat-form-field class="flex-50-minus-25px">
<mat-label>{{ 'labels.inputs.Reason for Not Having PAN' | translate }}</mat-label>
<mat-select formControlName="reasonForNoPan">
@for (reason of reasonOptions; track reason.value) {
<mat-option [value]="reason.value">{{ reason.key | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<!-- Declaration Checkbox -->
<div class="form60-declaration">
<mat-checkbox formControlName="declaration">
{{ 'labels.text.I declare that I do not have PAN and information provided is correct.' | translate }}
</mat-checkbox>
</div>
<!-- Action Buttons -->
<div class="layout-row gap-32px layout-align-center">
@if (form60Data) {
<button type="button" mat-raised-button (click)="toggleEditMode()">
{{ 'labels.buttons.Cancel' | translate }}
</button>
}
<button type="submit" mat-raised-button color="primary" [disabled]="!form60Form.valid">
{{ 'labels.buttons.Submit' | translate }}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
}
}
</div>
@@ -0,0 +1,84 @@
/**
* 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/.
*/
.form60-container {
padding: 16px;
}
.form60-notice {
display: flex;
align-items: center;
gap: 10px;
background: #fff3cd;
padding: 12px 16px;
border-left: 5px solid #fc0;
border-radius: 4px;
margin-bottom: 16px;
font-size: 14px;
color: #856404;
mat-icon {
color: #856404;
}
}
.form60-loading {
display: flex;
justify-content: center;
padding: 32px;
}
.form60-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px 0;
}
.form60-field {
.label {
font-size: 12px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
display: block;
}
.value {
font-size: 14px;
color: #333;
margin-top: 4px;
display: block;
}
}
.form60-declaration {
margin: 16px 0 24px;
padding: 12px 16px;
background: #f5f5f5;
border-radius: 6px;
}
@media screen and (width >= 960px) {
.layout-row-wrap > .flex-fill:nth-child(odd),
.layout-row-wrap > .flex-50-minus-25px:nth-child(odd) {
margin-right: 25px;
}
.layout-row-wrap > .flex-fill:last-child,
.layout-row-wrap > .flex-50-minus-25px:last-child {
margin-right: 0;
}
}
@media (width <= 768px) {
.form60-summary {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,194 @@
/**
* 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 } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subject, takeUntil, catchError, of } from 'rxjs';
/** Custom Services */
import { ClientsService } from '../../clients.service';
import { AlertService } from 'app/core/alert/alert.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
@Component({
selector: 'mifosx-form60-tab',
templateUrl: './form60-tab.component.html',
styleUrls: ['./form60-tab.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatIcon,
MatProgressSpinner,
DateFormatPipe
]
})
export class Form60TabComponent implements OnInit, OnDestroy {
private formBuilder = inject(UntypedFormBuilder);
private route = inject(ActivatedRoute);
private clientsService = inject(ClientsService);
private alertService = inject(AlertService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private destroy$ = new Subject<void>();
/** Client info */
clientId: string;
/** State */
loading = true;
form60Data: any = null;
isEditMode = false;
/** Maximum date allowed */
maxDate = new Date();
/** Form */
form60Form: UntypedFormGroup;
/** Dropdown options */
transactionTypes = [
{ value: 'Account Opening', key: 'labels.inputs.Account Opening' },
{ value: 'Cash Deposit', key: 'labels.inputs.Cash Deposit' },
{ value: 'Fixed Deposit', key: 'labels.inputs.Fixed Deposit' },
{ value: 'Loan Processing', key: 'labels.inputs.Loan Processing' }
];
reasonOptions = [
{ value: 'Applied for PAN', key: 'labels.inputs.Applied for PAN' },
{ value: 'Income below taxable limit', key: 'labels.inputs.Income below taxable limit' },
{ value: 'Other', key: 'labels.inputs.Other' }
];
ngOnInit(): void {
this.maxDate = this.settingsService.businessDate;
this.createForm();
this.route.parent.params.subscribe((params: any) => {
this.clientId = params['clientId'];
this.loadForm60Data();
});
}
private createForm(): void {
this.form60Form = this.formBuilder.group({
fullName: [
'',
Validators.required
],
dateOfBirth: [''],
fatherName: [''],
address: [''],
transactionType: ['Account Opening'],
transactionAmount: [''],
estimatedTotalIncome: [''],
reasonForNoPan: ['Applied for PAN'],
declaration: [
false,
Validators.requiredTrue
]
});
}
private loadForm60Data(): void {
this.loading = true;
this.clientsService
.getClientForm60(this.clientId)
.pipe(
takeUntil(this.destroy$),
catchError(() => {
this.loading = false;
return of(null);
})
)
.subscribe((data: any) => {
this.loading = false;
this.form60Data = data;
if (data) {
this.populateForm(data);
} else {
this.isEditMode = true;
}
});
}
private populateForm(data: any): void {
this.form60Form.patchValue({
fullName: data.fullName || '',
dateOfBirth: data.dateOfBirth ? this.dateUtils.formatDate(data.dateOfBirth, 'yyyy-MM-dd') : '',
fatherName: data.fatherName || '',
address: data.address || '',
transactionType: data.transactionType || 'Account Opening',
transactionAmount: data.transactionAmount || '',
estimatedTotalIncome: data.estimatedTotalIncome || '',
reasonForNoPan: data.reasonForNoPan || 'Applied for PAN',
declaration: data.declaration || false
});
}
toggleEditMode(): void {
this.isEditMode = !this.isEditMode;
if (!this.isEditMode && this.form60Data) {
this.populateForm(this.form60Data);
}
}
submit(): void {
const formValue = { ...this.form60Form.value };
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
if (formValue.dateOfBirth) {
if (formValue.dateOfBirth instanceof Date) {
formValue.dateOfBirth = this.dateUtils.formatDate(formValue.dateOfBirth, dateFormat);
}
} else {
delete formValue.dateOfBirth;
}
const data = {
...formValue,
dateFormat,
locale
};
this.clientsService
.submitClientForm60(this.clientId, data)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (response: any) => {
this.form60Data = response || data;
this.isEditMode = false;
this.alertService.alert({
type: 'success',
message: 'Form 60 submitted successfully.'
});
},
error: (err: any) => {
this.alertService.alert({
type: 'error',
message: err?.error?.developerMessage || err?.error?.defaultUserMessage || 'Failed to submit Form 60.'
});
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}