update code account saving,current,RD

This commit is contained in:
Nitesh Yadav
2026-04-24 16:24:28 +05:30
parent fc1c5126fc
commit fbaec18552
49 changed files with 4221 additions and 2260 deletions
@@ -35,7 +35,7 @@
</ng-template>
<!-- Step 1: Details -->
<mat-step [stepControl]="currentAccountDetailsForm">
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-current-account-details-step
@@ -45,64 +45,48 @@
</mifosx-current-account-details-step>
</mat-step>
<!-- Step 2: Currency -->
<mat-step>
<ng-template matStepLabel>CURRENCY</ng-template>
@if (currentAccountProductTemplate) {
<!-- Step 2: Currency -->
<mat-step>
<ng-template matStepLabel>CURRENCY</ng-template>
<mifosx-current-account-currency-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-currency-step>
</mat-step>
<!-- Step 3: Term (Interest Rate) -->
<mat-step [stepControl]="currentAccountTermForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-current-account-term-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-term-step>
</mat-step>
<!-- Step 4: Settings -->
<mat-step>
<ng-template matStepLabel>SETTINGS</ng-template>
<mifosx-current-account-settings-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-settings-step>
</mat-step>
<!-- Step 5: Digital -->
<mat-step>
<ng-template matStepLabel>DIGITAL</ng-template>
<mifosx-current-account-digital-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-digital-step>
</mat-step>
<!-- Step 6: Charges -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-current-account-charges-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
[currentAccountTemplate]="currentAccountTemplate"
[currencyCode]="currentAccountTermsForm.get('currencyCode')"
>
</mifosx-current-account-charges-step>
<!-- Hidden terms step to keep currencyCode form populated -->
<div style="display: none">
<mifosx-current-account-terms-step
<mifosx-current-account-currency-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
[currentAccountTemplate]="currentAccountTemplate"
></mifosx-current-account-terms-step>
</div>
</mat-step>
></mifosx-current-account-currency-step>
<!-- Step 7: Preview -->
@if (currentAccountFormValid) {
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 3: Digital -->
<mat-step>
<ng-template matStepLabel>DIGITAL</ng-template>
<mifosx-current-account-digital-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-digital-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 4: Preview -->
<mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-current-account-preview-step
@@ -27,11 +27,14 @@
.mat-step-content {
padding: 1.5rem;
margin: 1rem 0;
overflow: visible !important;
}
}
// Form field spacing within steps
:host ::ng-deep mat-step-content {
overflow: visible !important;
mat-form-field {
margin-bottom: 1rem;
width: 100%;
@@ -12,10 +12,6 @@ import { Router, ActivatedRoute } from '@angular/router';
/** Custom Components */
import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component';
import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component';
import { CurrentAccountTermStepComponent } from '../current-account-stepper/current-account-term-step/current-account-term-step.component';
import { CurrentAccountSettingsStepComponent } from '../current-account-stepper/current-account-settings-step/current-account-settings-step.component';
import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component';
import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component';
import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component';
@@ -23,10 +19,18 @@ import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/c
import { CurrentAccountsService } from '../current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import {
MatStepper,
MatStepperIcon,
MatStep,
MatStepLabel,
MatStepperNext,
MatStepperPrevious
} from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FormGroup } from '@angular/forms';
/**
* Create Current Account Component
@@ -39,16 +43,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
MatStepperNext,
MatStepperPrevious,
FaIconComponent,
MatStep,
MatStepLabel,
CurrentAccountDetailsStepComponent,
CurrentAccountCurrencyStepComponent,
CurrentAccountTermStepComponent,
CurrentAccountSettingsStepComponent,
CurrentAccountDigitalStepComponent,
CurrentAccountTermsStepComponent,
CurrentAccountChargesStepComponent,
CurrentAccountPreviewStepComponent
]
})
@@ -67,18 +69,10 @@ export class CreateCurrentAccountComponent {
/** Current Account Details Step */
@ViewChild(CurrentAccountDetailsStepComponent, { static: true })
currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
/** Current Account Term Step (new) */
@ViewChild(CurrentAccountTermStepComponent, { static: true })
currentAccountTermStep: CurrentAccountTermStepComponent;
/** Current Account Settings Step */
@ViewChild(CurrentAccountSettingsStepComponent, { static: true })
currentAccountSettingsStep: CurrentAccountSettingsStepComponent;
/** Current Account Terms Step (old — kept for currencyCode access) */
@ViewChild(CurrentAccountTermsStepComponent, { static: true })
currentAccountTermsStep: CurrentAccountTermsStepComponent;
/** Current Account Charges Step */
@ViewChild(CurrentAccountChargesStepComponent, { static: true })
currentAccountChargesStep: CurrentAccountChargesStepComponent;
/** Current Account Currency Step */
@ViewChild(CurrentAccountCurrencyStepComponent, { static: true })
currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent;
constructor() {
this.route.data.subscribe((data: { currentAccountTemplate: any }) => {
@@ -100,32 +94,17 @@ export class CreateCurrentAccountComponent {
return this.currentAccountDetailsStep.currentAccountDetailsForm;
}
/**
* Retrieves new term form (interest rate).
*/
get currentAccountTermForm() {
return this.currentAccountTermStep.currentAccountTermForm;
/** Stub forms needed by preview step inputs */
get currentAccountTermForm(): FormGroup {
return new FormGroup({});
}
/**
* Retrieves settings form.
*/
get currentAccountSettingsForm() {
return this.currentAccountSettingsStep.currentAccountSettingsForm;
get currentAccountSettingsForm(): FormGroup {
return new FormGroup({});
}
/**
* Retrieves old terms form — still used for currencyCode by charges step.
*/
get currentAccountTermsForm() {
return this.currentAccountTermsStep.currentAccountTermsForm;
}
/**
* Checks validity of overall current account form.
*/
get currentAccountFormValid() {
return this.currentAccountDetailsForm.valid && this.currentAccountTermForm.valid;
get currentAccountTermsForm(): FormGroup {
return new FormGroup({});
}
/**
@@ -133,10 +112,7 @@ export class CreateCurrentAccountComponent {
*/
get currentAccount() {
return {
...this.currentAccountDetailsStep.currentAccountDetails,
...this.currentAccountTermStep.currentAccountTerms,
...this.currentAccountSettingsStep.currentAccountSettings,
...this.currentAccountChargesStep.currentAccountCharges
...this.currentAccountDetailsStep.currentAccountDetails
};
}
@@ -149,15 +125,8 @@ export class CreateCurrentAccountComponent {
const monthDayFormat = 'dd MMMM';
const currentAccount = {
...this.currentAccount,
charges: this.currentAccount.charges.map((charge: any) => ({
chargeId: charge.id,
amount: charge.amount,
dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate,
feeOnMonthDay: charge.feeOnMonthDay
? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat)
: charge.feeOnMonthDay,
feeInterval: charge.feeInterval
})),
...this.currentAccountCurrencyStep.currencyStepData,
charges: [],
submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat),
dateFormat,
monthDayFormat,
@@ -26,6 +26,30 @@
</mat-form-field>
</div>
<div class="sac-alert sac-alert--editable">
<div class="sac-alert__content">
<h4>Balance Swap Option</h4>
<p>Allows balance to be swapped or transferred between linked accounts as per product configuration.</p>
</div>
<mat-slide-toggle [(ngModel)]="balanceSwapOption" color="primary"></mat-slide-toggle>
</div>
<div class="sac-alert sac-alert--editable">
<div class="sac-alert__content">
<h4>MSME</h4>
<p>Account is categorized under the Micro, Small and Medium Enterprises segment.</p>
</div>
<mat-slide-toggle [(ngModel)]="msme" color="primary"></mat-slide-toggle>
</div>
<div class="sac-alert sac-alert--editable">
<div class="sac-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic debit instructions for recurring payments linked to this account.</p>
</div>
<mat-slide-toggle [(ngModel)]="autoDebitFacility" color="primary"></mat-slide-toggle>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Dormancy Alert</h4>
@@ -36,25 +60,7 @@
</span>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Balance Swap Option</h4>
<p>Allows balance to be swapped or transferred between linked accounts as per product configuration.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.balanceSwapOption">
{{ currentAccountProductTemplate?.balanceSwapOption ? 'Yes' : 'No' }}
</span>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>MSME</h4>
<p>Account is categorized under the Micro, Small and Medium Enterprises segment.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.msme">
{{ currentAccountProductTemplate?.msme ? 'Yes' : 'No' }}
</span>
</div>
<mifosx-dormancy-alert-panel [productTemplate]="currentAccountProductTemplate"></mifosx-dormancy-alert-panel>
<div class="sac-alert">
<div class="sac-alert__content">
@@ -66,14 +72,8 @@
</span>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic debit instructions for recurring payments linked to this account.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.autoDebitFacility">
{{ currentAccountProductTemplate?.autoDebitFacility ? 'Yes' : 'No' }}
</span>
</div>
@if (currentAccountProductTemplate?.nominationFacility) {
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
}
</div>
</div>
@@ -114,3 +114,17 @@ $gray-900: #202124;
border-color: #bbf7d0;
}
}
.sac-alert--editable {
border-color: #bae6fd;
background: #f0f9ff;
&:hover {
border-color: #7dd3fc;
box-shadow: 0 2px 8px rgb(2 132 199 / 10%);
}
h4 {
color: #0369a1;
}
}
@@ -6,15 +6,55 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component';
import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'mifosx-current-account-currency-step',
templateUrl: './current-account-currency-step.component.html',
styleUrls: ['./current-account-currency-step.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
imports: [
...STANDALONE_SHARED_IMPORTS,
NomineeFormComponent,
DormancyAlertPanelComponent,
MatSlideToggle,
FormsModule
]
})
export class CurrentAccountCurrencyStepComponent {
export class CurrentAccountCurrencyStepComponent implements OnChanges {
@Input() currentAccountProductTemplate: any;
nomineeData: any = null;
// Editable feature toggles (pre-filled from product, overridable by user)
balanceSwapOption = false;
msme = false;
autoDebitFacility = false;
ngOnChanges(changes: SimpleChanges): void {
if (changes['currentAccountProductTemplate']?.currentValue) {
const pt = changes['currentAccountProductTemplate'].currentValue;
this.balanceSwapOption = !!pt.balanceSwapOption;
this.msme = !!pt.msme;
this.autoDebitFacility = !!pt.autoDebitFacility;
}
}
onNomineeChange(event: any): void {
this.nomineeData = event;
}
/** Called by parent to collect currency step values */
get currencyStepData() {
return {
balanceSwapOption: this.balanceSwapOption,
msme: this.msme,
autoDebitFacility: this.autoDebitFacility,
...(this.nomineeData ? { nominee: this.nomineeData } : {})
};
}
}
@@ -7,7 +7,7 @@
-->
<form [formGroup]="currentAccountDetailsForm">
<!-- Account Details -->
<!-- ══ Account Details ══════════════════════════════════════════ -->
<div class="sad-section">
<div class="sad-section__header">
<i class="fas fa-info-circle"></i>
@@ -45,9 +45,7 @@
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId">
@@ -56,9 +54,7 @@
}
</mat-select>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
@@ -67,8 +63,8 @@
</div>
</div>
<!-- Product Classification (read-only, shown after product is selected) -->
@if (currentAccountProductSelected && productTemplate) {
<!-- ══ Product Classification (read-only) ══════════════════════ -->
<div class="sad-section sad-section--mt">
<div class="sad-section__header">
<i class="fas fa-tags"></i>
@@ -125,6 +121,10 @@
</div>
</div>
</div>
@if (productTemplate?.introducerRequired) {
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
}
}
<div class="layout-row align-center gap-2percent margin-t responsive-column">
@@ -15,7 +15,7 @@ $gray-900: #202124;
.sad-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
overflow: visible;
background: #fff;
&--mt {
@@ -103,3 +103,61 @@ $gray-900: #202124;
border-color: #bbf7d0;
}
}
// ── Account Feature Toggles ────────────────────────────────────────────────
.sad-toggles {
display: flex;
flex-direction: column;
gap: 10px;
padding: 4px 0;
}
.sad-toggle-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
background: #fff;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:hover {
border-color: #93c5fd;
box-shadow: 0 1px 6px rgb(59 130 246 / 10%);
}
&__left {
display: flex;
align-items: center;
gap: 12px;
> i {
width: 36px;
height: 36px;
border-radius: 8px;
background: #f0f9ff;
border: 1px solid #bae6fd;
color: #0284c7;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
flex: 0 0 auto;
}
}
&__label {
font-size: 13.5px;
font-weight: 600;
color: #1e293b;
}
&__hint {
font-size: 11.5px;
color: #64748b;
margin-top: 2px;
}
}
@@ -17,6 +17,7 @@ import { SettingsService } from 'app/settings/settings.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
/**
* Current Account Details Step
@@ -29,7 +30,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
MatStepperNext,
IntroducerSearchComponent
]
})
export class CurrentAccountDetailsStepComponent implements OnInit {
@@ -56,6 +58,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
currentAccountDetailsForm: UntypedFormGroup;
currentAccountProductSelected = false;
introducerId: number | null = null;
/** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter();
@@ -102,7 +105,10 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
Validators.required
],
fieldOfficerId: [''],
externalId: ['']
externalId: [''],
balanceSwapOption: [false],
msme: [false],
autoDebitFacility: [false]
});
}
@@ -131,6 +137,12 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
this.currentAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.currentAccountProductSelected = true;
// Pre-populate toggles from product defaults (user can override)
this.currentAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
autoDebitFacility: !!response.autoDebitFacility
});
this.cdr.detectChanges();
if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) {
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId);
@@ -166,7 +178,15 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
/**
* Returns current account form value.
*/
onIntroducerChange(event: { clientId: number; displayName: string } | null): void {
this.introducerId = event?.clientId ?? null;
}
get currentAccountDetails() {
return this.currentAccountDetailsForm.getRawValue();
const value = this.currentAccountDetailsForm.getRawValue();
if (this.introducerId) {
value.introducerId = this.introducerId;
}
return value;
}
}
@@ -6,229 +6,209 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="spl-section">
<div class="spl-section__header">
<span class="spl-section__icon">
<i class="fas fa-chart-bar"></i>
</span>
<div class="spl-section__title">Transaction &amp; Product Limits</div>
<div class="spl-section__badge">FROM PRODUCT</div>
<div class="dig-wrapper">
<div class="dig-header">
<div class="dig-header__icon">
<i class="fas fa-mobile-alt"></i>
</div>
<div class="dig-header__text">
<h3 class="dig-header__title">Digital &amp; Account Features</h3>
<p class="dig-header__sub">Toggle the facilities available for this account (as per RBI guidelines)</p>
</div>
<span class="dig-header__badge">FROM PRODUCT</span>
</div>
<div class="spl-section__body">
<div class="spl-features">
<div class="spl-features__header">Account Features (as per RBI guidelines)</div>
<div class="spl-features__grid">
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-credit-card"></i> ATM Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.atmFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-wallet"></i> ATM Withdrawal</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.atmWithdrawalFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-mobile-alt"></i> UPI Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.upiFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-exchange-alt"></i> POS Transactions</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.posTransactionsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-book"></i> Cheque Book</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.chequeBook" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-bolt"></i> IMPS Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.impsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-sms"></i> SMS Alerts</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.smsAlerts" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-bell"></i> BBPS Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.bbpsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-shield-alt"></i> Insurance Cover</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.insuranceCover" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-bolt"></i> BEPG Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.bepgFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-money-check-alt"></i> NEFT Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.neftFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-money-check"></i> RTGS Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.rtgsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-balance-scale"></i> Inter-Bank Total Cap</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.interBankTotalCapFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-arrow-right-arrow-left"></i> Withdrawal Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.withdrawalFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-id-card"></i> Deposit Facility (KYC)</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.depositFacilityKyc" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-credit-card"></i> Virtual Debit Card</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.virtualDebitCard" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<div class="dig-grid">
<div class="dig-card" [class.dig-card--on]="facilityConfig.atmFacility">
<div class="dig-card__icon"><i class="fas fa-credit-card"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">ATM Facility</span>
<span class="dig-card__status">{{ facilityConfig.atmFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.atmFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
@if (accountFacilityLimits.length > 0) {
<div class="spl-table-wrap">
<table class="spl-table">
<colgroup>
<col class="spl-table__col--label" />
<col class="spl-table__col--value" />
<col class="spl-table__col--value" />
<col class="spl-table__col--value" />
</colgroup>
<thead>
<tr>
<th>Limit Type</th>
<th>General Limit (Daily)</th>
<th>Minor Account Limit</th>
<th>Small Saving Limit</th>
</tr>
</thead>
<tbody>
@for (row of accountFacilityLimits; track row.limitType) {
<tr>
<td>{{ row.limitType }}</td>
<td><input type="text" class="spl-input" [value]="row.generalLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.minorAccountLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.smallSavingLimit" disabled /></td>
</tr>
}
</tbody>
</table>
<div class="dig-card" [class.dig-card--on]="facilityConfig.atmWithdrawalFacility">
<div class="dig-card__icon"><i class="fas fa-wallet"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">ATM Withdrawal</span>
<span class="dig-card__status">{{ facilityConfig.atmWithdrawalFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
}
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.atmWithdrawalFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
@if (depositLimitKyc?.general) {
<div class="spl-table-wrap spl-table-wrap--kyc">
<h4 class="mat-h4">Deposit Limit (KYC)</h4>
<table class="spl-table">
<thead>
<tr>
<th>Account Type</th>
<th>KYC Type</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.general.kycType" disabled>
<option>{{ depositLimitKyc.general.kycType }}</option>
</select>
</td>
<td><input type="text" class="spl-input" [value]="depositLimitKyc.general.limit" disabled /></td>
</tr>
<tr>
<td>Minor</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.minor.kycType" disabled>
<option>{{ depositLimitKyc.minor.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.minor.limit" disabled /></td>
</tr>
<tr>
<td>Small Saving</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.smallSaving.kycType" disabled>
<option>{{ depositLimitKyc.smallSaving.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.smallSaving.limit" disabled /></td>
</tr>
</tbody>
</table>
<div class="dig-card" [class.dig-card--on]="facilityConfig.upiFacility">
<div class="dig-card__icon"><i class="fas fa-mobile-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">UPI Facility</span>
<span class="dig-card__status">{{ facilityConfig.upiFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
}
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.upiFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="spl-note">
<strong>Note:</strong>
These facilities are defined at product level and applied to this account. Small Saving Account limits are
strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular.
<div class="dig-card" [class.dig-card--on]="facilityConfig.posTransactionsFacility">
<div class="dig-card__icon"><i class="fas fa-exchange-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">POS Transactions</span>
<span class="dig-card__status">{{ facilityConfig.posTransactionsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.posTransactionsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.chequeBook">
<div class="dig-card__icon"><i class="fas fa-book"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Cheque Book</span>
<span class="dig-card__status">{{ facilityConfig.chequeBook ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.chequeBook" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.impsFacility">
<div class="dig-card__icon"><i class="fas fa-bolt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">IMPS Facility</span>
<span class="dig-card__status">{{ facilityConfig.impsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.impsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.smsAlerts">
<div class="dig-card__icon"><i class="fas fa-sms"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">SMS Alerts</span>
<span class="dig-card__status">{{ facilityConfig.smsAlerts ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.smsAlerts" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.bbpsFacility">
<div class="dig-card__icon"><i class="fas fa-bell"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">BBPS Facility</span>
<span class="dig-card__status">{{ facilityConfig.bbpsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.bbpsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.insuranceCover">
<div class="dig-card__icon"><i class="fas fa-shield-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Insurance Cover</span>
<span class="dig-card__status">{{ facilityConfig.insuranceCover ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.insuranceCover" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.bepgFacility">
<div class="dig-card__icon"><i class="fas fa-plug"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">BEPG Facility</span>
<span class="dig-card__status">{{ facilityConfig.bepgFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.bepgFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.neftFacility">
<div class="dig-card__icon"><i class="fas fa-money-check-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">NEFT Facility</span>
<span class="dig-card__status">{{ facilityConfig.neftFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.neftFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.rtgsFacility">
<div class="dig-card__icon"><i class="fas fa-money-check"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">RTGS Facility</span>
<span class="dig-card__status">{{ facilityConfig.rtgsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.rtgsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.interBankTotalCapFacility">
<div class="dig-card__icon"><i class="fas fa-balance-scale"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Inter-Bank Total Cap</span>
<span class="dig-card__status">{{ facilityConfig.interBankTotalCapFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.interBankTotalCapFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.withdrawalFacility">
<div class="dig-card__icon"><i class="fas fa-arrow-right-arrow-left"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Withdrawal Facility</span>
<span class="dig-card__status">{{ facilityConfig.withdrawalFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.withdrawalFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.depositFacilityKyc">
<div class="dig-card__icon"><i class="fas fa-id-card"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Deposit Facility (KYC)</span>
<span class="dig-card__status">{{ facilityConfig.depositFacilityKyc ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.depositFacilityKyc" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.virtualDebitCard">
<div class="dig-card__icon"><i class="fas fa-credit-card"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Virtual Debit Card</span>
<span class="dig-card__status">{{ facilityConfig.virtualDebitCard ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.virtualDebitCard" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
</div>
</div>
@@ -6,194 +6,205 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$border: #d9e2ec;
$border-strong: #c7d2de;
$text: #24364b;
$blue: #2563eb;
$blue-light: #eff6ff;
$green: #16a34a;
$green-light: #f0fdf4;
$green-border: #bbf7d0;
$border: #e2e8f0;
$text: #1e293b;
$muted: #64748b;
$header-bg: #f8fafc;
$gray-bg: #f8fafc;
.spl-section {
// ── Wrapper ──────────────────────────────────────────────
.dig-wrapper {
border: 1px solid $border;
border-radius: 12px;
border-radius: 14px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
}
.spl-section__header {
// ── Header ───────────────────────────────────────────────
.dig-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: $header-bg;
gap: 14px;
padding: 18px 24px;
background: $gray-bg;
border-bottom: 1px solid $border;
}
.spl-section__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 8px;
background: #eff6ff;
color: #2563eb;
font-size: 14px;
flex: 0 0 auto;
}
.spl-section__title {
flex: 1 1 auto;
font-size: 16px;
font-weight: 600;
color: $text;
}
.spl-section__badge {
flex: 0 0 auto;
padding: 5px 10px;
border-radius: 999px;
background: #dbeafe;
color: #1d4ed8;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
white-space: nowrap;
}
.spl-section__body {
padding: 18px 20px 20px;
}
.spl-table-wrap {
overflow-x: auto;
margin-top: 16px;
}
.spl-table {
width: 100%;
min-width: 0;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid $border;
col.spl-table__col--label {
width: 22%;
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
background: $blue-light;
color: $blue;
font-size: 16px;
flex: 0 0 auto;
}
col.spl-table__col--value {
width: 26%;
&__text {
flex: 1;
}
th,
td {
border: 1px solid $border;
padding: 6px 8px;
vertical-align: middle;
}
th {
background: #f8fafc;
color: #64748b;
text-align: center;
font-size: 11px;
&__title {
margin: 0 0 2px;
font-size: 15px;
font-weight: 700;
}
td:first-child {
background: #f8fafc;
font-weight: 600;
color: $text;
white-space: nowrap;
}
td {
font-size: 13px;
overflow: hidden;
&__sub {
margin: 0;
font-size: 12px;
color: $muted;
}
&__badge {
flex: 0 0 auto;
padding: 4px 12px;
border-radius: 999px;
background: #dbeafe;
color: $blue;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
}
}
.spl-input,
.spl-select {
width: 100%;
box-sizing: border-box;
font: inherit;
}
.spl-input {
height: 30px;
border: 1px solid $border-strong;
border-radius: 2px;
padding: 0 8px;
background: #f8fafc;
color: $muted;
}
.spl-select {
height: 30px;
border: 1px solid $border-strong;
border-radius: 2px;
padding: 0 6px;
background: #f8fafc;
color: $muted;
}
.spl-features {
margin-bottom: 16px;
}
.spl-features__header {
font-size: 13px;
font-weight: 600;
color: $muted;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.spl-features__grid {
// ── Grid ─────────────────────────────────────────────────
.dig-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 16px;
grid-template-columns: repeat(3, 1fr);
gap: 0;
@media (width <= 900px) {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 960px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 640px) {
@media (width <= 580px) {
grid-template-columns: 1fr;
}
}
.spl-feature {
// ── Card ─────────────────────────────────────────────────
.dig-card {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
color: $text;
}
.spl-feature__label {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #334155;
gap: 12px;
padding: 14px 18px;
border-right: 1px solid $border;
border-bottom: 1px solid $border;
background: #fff;
transition: background 0.18s ease;
cursor: pointer;
i {
color: #2563eb;
font-size: 12px;
width: 14px;
text-align: center;
&:nth-child(3n) {
border-right: none;
}
&--on {
background: $green-light;
.dig-card__icon {
background: $green-border;
color: $green;
}
.dig-card__status {
color: $green;
font-weight: 600;
}
}
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 9px;
background: $blue-light;
color: $blue;
font-size: 14px;
flex: 0 0 auto;
transition:
background 0.18s,
color 0.18s;
}
&__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
&__label {
font-size: 13px;
font-weight: 600;
color: $text;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__status {
font-size: 11px;
color: $muted;
transition: color 0.18s;
}
}
.spl-note {
margin-top: 16px;
padding: 12px 14px;
border: 1px solid $border;
border-radius: 8px;
background: #f8fafc;
color: $muted;
font-size: 11px;
line-height: 1.6;
// ── Toggle Switch ─────────────────────────────────────────
.dig-toggle {
position: relative;
flex: 0 0 auto;
cursor: pointer;
input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
&:checked + .dig-toggle__track {
background: $green;
border-color: $green;
}
&:checked + .dig-toggle__track .dig-toggle__thumb {
transform: translateX(18px);
}
}
&__track {
display: block;
width: 40px;
height: 22px;
border-radius: 999px;
background: #cbd5e1;
border: 1.5px solid #94a3b8;
position: relative;
transition:
background 0.2s,
border-color 0.2s;
}
&__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 20%);
transition: transform 0.2s ease;
}
}
@@ -39,31 +39,16 @@ const DEFAULT_FACILITY_CONFIG = {
]
})
export class CurrentAccountDigitalStepComponent {
private _currentAccountProductTemplate: any;
facilityConfig = { ...DEFAULT_FACILITY_CONFIG };
@Input()
set currentAccountProductTemplate(value: any) {
this._currentAccountProductTemplate = value;
}
get currentAccountProductTemplate(): any {
return this._currentAccountProductTemplate;
if (value?.accountFacilityConfig) {
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
}
}
get accountFacilityConfig() {
return this._currentAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG;
}
get accountFacilityLimits(): any[] {
return this._currentAccountProductTemplate?.accountFacilityLimits || [];
}
get depositLimitKyc() {
return (
this._currentAccountProductTemplate?.depositLimitKyc || {
general: { kycType: '', limit: '' },
minor: { kycType: '', limit: '' },
smallSaving: { kycType: '', limit: '' }
}
);
get currentAccountDigital() {
return { accountFacilityConfig: { ...this.facilityConfig } };
}
}
@@ -218,174 +218,6 @@
</div>
</div>
<!-- Terms -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Terms' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Interest Posting Frequency</span>
<span class="preview-value">{{
termPreview?.interestPostingPeriodType
| find: currentAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">Auto Interest Post</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="termPreview?.autoPost">
{{ termPreview?.autoPost | yesNo }}
</span>
</div>
@if (termPreview?.frequencyFixDay !== undefined && termPreview?.frequencyFixDay !== null) {
<div class="preview-field">
<span class="preview-label">Frequency Fix Day</span>
<span class="preview-value">{{ termPreview?.frequencyFixDay }} Days</span>
</div>
}
@if (termPreview?.lastInterestDate) {
<div class="preview-field">
<span class="preview-label">Last Interest Date</span>
<span class="preview-value">{{ termPreview?.lastInterestDate | dateFormat }}</span>
</div>
}
</div>
</div>
</div>
<!-- Settings -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Settings' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (
settingsPreview?.minRequiredOpeningBalance !== undefined &&
settingsPreview?.minRequiredOpeningBalance !== null &&
settingsPreview?.minRequiredOpeningBalance !== ''
) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Opening Balance' | translate }}</span>
<span class="preview-value">{{ settingsPreview?.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.enforceMinRequiredBalance"
>
{{ settingsPreview?.enforceMinRequiredBalance | yesNo }}
</span>
</div>
@if (
settingsPreview?.minRequiredBalance !== undefined &&
settingsPreview?.minRequiredBalance !== null &&
settingsPreview?.minRequiredBalance !== ''
) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Balance' | translate }}</span>
<span class="preview-value">{{ settingsPreview?.minRequiredBalance | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Balance Frequency</span>
<span class="preview-value">{{ settingsPreview?.balanceFrequency || 'Unassigned' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.withdrawalFeeForTransfers"
>
{{ settingsPreview?.withdrawalFeeForTransfers | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Enable Lock-in Period</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.enableLockinPeriod || !!settingsPreview?.lockinPeriodFrequency"
>
{{ settingsPreview?.enableLockinPeriod || !!settingsPreview?.lockinPeriodFrequency | yesNo }}
</span>
</div>
@if (settingsPreview?.lockinPeriodFrequency) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Lock-in Period' | translate }}</span>
<span class="preview-value">
{{ settingsPreview?.lockinPeriodFrequency }}
{{
settingsPreview?.lockinPeriodFrequencyType
| find: currentAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}
</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Account Closed Zero Balance In Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.accountClosedZeroBalanceInDayClosing"
>
{{ settingsPreview?.accountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="settingsPreview?.allowOverdraft">
{{ settingsPreview?.allowOverdraft | yesNo }}
</span>
</div>
@if (settingsPreview?.allowOverdraft) {
<div class="preview-field">
<span class="preview-label">Avg Balance Calc Period</span>
<span class="preview-value">{{ settingsPreview?.avgBalanceCalcPeriod || 'Unassigned' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Valid For Period</span>
<span class="preview-value">{{ settingsPreview?.validForPeriod || 'Unassigned' }}</span>
</div>
@if (
settingsPreview?.overdraftLimit !== undefined &&
settingsPreview?.overdraftLimit !== null &&
settingsPreview?.overdraftLimit !== ''
) {
<div class="preview-field">
<span class="preview-label">OD Limit %</span>
<span class="preview-value">{{ settingsPreview?.overdraftLimit | formatNumber }}</span>
</div>
}
@if (
settingsPreview?.nominalAnnualInterestRateOverdraft !== undefined &&
settingsPreview?.nominalAnnualInterestRateOverdraft !== null &&
settingsPreview?.nominalAnnualInterestRateOverdraft !== ''
) {
<div class="preview-field">
<span class="preview-label">Interest on OD %</span>
<span class="preview-value">{{
settingsPreview?.nominalAnnualInterestRateOverdraft | formatNumber
}}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">OD Account Closed Zero Balance In Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.odAccountClosedZeroBalanceInDayClosing"
>
{{ settingsPreview?.odAccountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
}
</div>
</div>
</div>
@if (hasDigitalSection) {
<div class="preview-section">
<div class="preview-section__header">
@@ -405,111 +237,6 @@
</div>
}
</div>
@if (digitalPreview?.accountFacilityLimits?.length) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Limit Type</th>
<th>General Limit</th>
<th>Minor Account Limit</th>
<th>Small Saving Limit</th>
</tr>
</thead>
<tbody>
@for (row of digitalPreview.accountFacilityLimits; track row.limitType) {
<tr>
<td>{{ row.limitType }}</td>
<td>{{ row.generalLimit || 'Unassigned' }}</td>
<td>{{ row.minorAccountLimit || 'Unassigned' }}</td>
<td>{{ row.smallSavingLimit || 'Unassigned' }}</td>
</tr>
}
</tbody>
</table>
}
@if (digitalPreview?.depositLimitKyc?.general) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Account Type</th>
<th>KYC Type</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>{{ digitalPreview.depositLimitKyc.general.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.general.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Minor</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Small Saving</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.limit || 'Unassigned' }}</td>
</tr>
</tbody>
</table>
}
</div>
</div>
}
<!-- Charges -->
@if (currentAccount?.charges?.length) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.inputs.Charges' | translate }}</span>
</div>
<div class="preview-body">
<table class="mat-elevation-z1 preview-table" mat-table [dataSource]="currentAccount.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name + ', ' + charge.currency?.displaySymbol }}</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType?.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType?.value }}</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (
charge.chargeTimeType?.value === 'Specified due date' || charge.chargeTimeType?.value === 'Weekly Fee'
) {
<span>{{ (charge.dueDate | dateFormat) || 'Unassigned' }}</span>
} @else if (
charge.chargeTimeType?.value === 'Monthly Fee' || charge.chargeTimeType?.value === 'Annual Fee'
) {
<span>{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}</span>
} @else {
<span>N/A</span>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.feeInterval || 'Not Provided' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
</div>
}
@@ -49,13 +49,7 @@
<mat-form-field appearance="outline" class="sat-field" (click)="lastInterestDatePicker.open()">
<mat-label>Last Interest Date</mat-label>
<input
matInput
[matDatepicker]="lastInterestDatePicker"
formControlName="lastInterestDate"
[min]="minDate"
[max]="maxDate"
/>
<input matInput [matDatepicker]="lastInterestDatePicker" formControlName="lastInterestDate" />
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
@@ -34,7 +34,8 @@
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="recurringDepositAccountDetailsForm">
<!-- Step 1: Details -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-recurring-deposits-account-details-step
@@ -44,49 +45,52 @@
</mifosx-recurring-deposits-account-details-step>
</mat-step>
<mat-step [stepControl]="recurringDepositAccountTermsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<!-- Step 2: Currency -->
@if (recurringDepositsAccountProductTemplate) {
<mat-step>
<ng-template matStepLabel>Currency</ng-template>
<mifosx-recurring-deposits-account-terms-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
>
</mifosx-recurring-deposits-account-terms-step>
</mat-step>
<mifosx-recurring-deposits-account-currency-step
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
>
</mifosx-recurring-deposits-account-currency-step>
<mat-step [stepControl]="recurringDepositAccountSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<mifosx-recurring-deposits-account-settings-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
>
</mifosx-recurring-deposits-account-settings-step>
</mat-step>
<!-- Step 3: Interest Rate Chart -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
</mat-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-recurring-deposits-account-charges-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountFormValid]="recurringDepositAccountFormValid"
>
</mifosx-recurring-deposits-account-charges-step>
</mat-step>
@if (recurringDepositAccountFormValid) {
<!-- Step 4: Preview -->
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-recurring-deposits-account-preview-step
@@ -5,3 +5,15 @@
* 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/.
*/
:host ::ng-deep mat-stepper {
background: transparent;
.mat-step-content {
overflow: visible !important;
}
}
:host ::ng-deep mat-step-content {
overflow: visible !important;
}
@@ -17,12 +17,17 @@ import { ProductsService } from 'app/products/products.service';
/** Step Components */
import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component';
import { RecurringDepositsAccountTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-terms-step/recurring-deposits-account-terms-step.component';
import { RecurringDepositsAccountSettingsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component';
import { RecurringDepositsAccountChargesStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component';
import { Dates } from 'app/core/utils/dates';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import {
MatStepper,
MatStepperIcon,
MatStep,
MatStepLabel,
MatStepperNext,
MatStepperPrevious
} from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component';
import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component';
import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -38,14 +43,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
MatStepperNext,
MatStepperPrevious,
FaIconComponent,
MatStep,
MatStepLabel,
RecurringDepositsAccountDetailsStepComponent,
RecurringDepositsAccountTermsStepComponent,
RecurringDepositsAccountSettingsStepComponent,
RecurringDepositsAccountCurrencyStepComponent,
RecurringDepositsAccountInterestRateChartStepComponent,
RecurringDepositsAccountChargesStepComponent,
RecurringDepositsAccountPreviewStepComponent
]
})
@@ -57,15 +62,11 @@ export class CreateRecurringDepositsAccountComponent {
private settingsService = inject(SettingsService);
private productsService = inject(ProductsService);
/** Imports all the step component */
@ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true })
recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent;
@ViewChild(RecurringDepositsAccountTermsStepComponent, { static: true })
recurringDepositAccountTermsStep: RecurringDepositsAccountTermsStepComponent;
@ViewChild(RecurringDepositsAccountSettingsStepComponent, { static: true })
recurringDepositAccountSettingsStep: RecurringDepositsAccountSettingsStepComponent;
@ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true })
recurringDepositAccountChargesStep: RecurringDepositsAccountChargesStepComponent;
@ViewChild(RecurringDepositsAccountCurrencyStepComponent, { static: true })
recurringDepositsAccountCurrencyStep: RecurringDepositsAccountCurrencyStepComponent;
/** Recurring Deposits Account Template */
recurringDepositsAccountTemplate: any;
@@ -83,7 +84,6 @@ export class CreateRecurringDepositsAccountComponent {
/**
* Sets recurring deposits account product template.
* Also fetches the active CBS base interest rate chart.
* @param {any} $event API response (includes productId)
*/
setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event;
@@ -102,42 +102,16 @@ export class CreateRecurringDepositsAccountComponent {
return this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetailsForm;
}
/** Get Recurring Deposit Account Terms Form Data */
get recurringDepositAccountTermsForm() {
return this.recurringDepositAccountTermsStep.recurringDepositAccountTermsForm;
}
/** Get Recurring Deposit Account Settings Form Data */
get recurringDepositAccountSettingsForm() {
return this.recurringDepositAccountSettingsStep.recurringDepositAccountSettingsForm;
}
/** Checks wheter all the forms in different steps are valid or not */
get recurringDepositAccountFormValid() {
return (
this.recurringDepositAccountDetailsForm.valid &&
this.recurringDepositAccountTermsForm.valid &&
this.recurringDepositAccountSettingsForm.valid
);
}
/** Retrieves Data of all the forms */
/** Retrieves Data of all forms */
get recurringDepositAccountData() {
return {
...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails,
...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms,
...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings,
...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges
...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails
};
}
/** Retrieves Data of all forms except Currency to submit the data */
get recurringDepositAccount() {
return {
...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails,
...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms,
...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings,
...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges
...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails
};
}
@@ -150,17 +124,10 @@ export class CreateRecurringDepositsAccountComponent {
const monthDayFormat = 'dd MMMM';
const recurringDepositAccount = {
...this.recurringDepositAccount,
...this.recurringDepositsAccountCurrencyStep.currencyStepData,
clientId: this.recurringDepositsAccountTemplate.clientId,
charges: this.recurringDepositAccount.charges.map((charge: any) => ({
chargeId: charge.id,
amount: charge.amount,
dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat),
feeOnMonthDay: charge.feeOnMonthDay,
feeInterval: charge.feeInterval
})),
isCalendarInherited: this.recurringDepositAccount.recurringDepositAccount
? this.recurringDepositAccount.recurringDepositAccount
: false,
charges: [],
isCalendarInherited: false,
submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat),
expectedFirstDepositOnDate: this.dateUtils.formatDate(
this.recurringDepositAccount.expectedFirstDepositOnDate,
@@ -0,0 +1,82 @@
<!--
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="racc-section">
<div class="racc-section__header">
<i class="fas fa-coins"></i>
<span>Currency &amp; Account Status Configuration</span>
<span class="racc-badge racc-badge--from">From Product</span>
</div>
<div class="racc-body">
<div class="racc-grid">
<mat-form-field appearance="outline" class="racc-field">
<mat-label>Currency</mat-label>
<input matInput [value]="recurringDepositsAccountProductTemplate?.currency?.name || ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="racc-field">
<mat-label>Decimal Places</mat-label>
<input matInput [value]="recurringDepositsAccountProductTemplate?.currency?.decimalPlaces ?? ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
</div>
<div class="racc-alert racc-alert--editable">
<div class="racc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-slide-toggle [(ngModel)]="balanceSwapOption" color="primary"></mat-slide-toggle>
</div>
<div class="racc-alert racc-alert--editable">
<div class="racc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises — mark this account under MSME category.</p>
</div>
<mat-slide-toggle [(ngModel)]="msme" color="primary"></mat-slide-toggle>
</div>
<div class="racc-alert racc-alert--editable">
<div class="racc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-slide-toggle [(ngModel)]="autoDebitFacility" color="primary"></mat-slide-toggle>
</div>
<div class="racc-alert">
<div class="racc-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<span class="racc-yn" [class.racc-yn--yes]="recurringDepositsAccountProductTemplate?.dormancyAlert">
{{ recurringDepositsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }}
</span>
</div>
<mifosx-dormancy-alert-panel
[productTemplate]="recurringDepositsAccountProductTemplate"
></mifosx-dormancy-alert-panel>
<div class="racc-alert">
<div class="racc-alert__content">
<h4>Nomination Facility</h4>
<p>Allow account holders to designate individuals to receive funds in the event of account holder's death.</p>
</div>
<span class="racc-yn" [class.racc-yn--yes]="recurringDepositsAccountProductTemplate?.nominationFacility">
{{ recurringDepositsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
</span>
</div>
@if (recurringDepositsAccountProductTemplate?.nominationFacility) {
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
}
</div>
</div>
@@ -0,0 +1,141 @@
/**
* 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/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.racc-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: $accent;
}
span {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.racc-body {
background: #fff;
}
.racc-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.racc-field {
width: 100%;
}
.racc-alert {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 18px 24px;
border-top: 1px solid $border;
background: #f8fafc;
@media (width <= 760px) {
flex-direction: column;
align-items: flex-start;
}
&__content {
h4 {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: #64748b;
max-width: 540px;
}
}
}
.racc-yn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
padding: 4px 14px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #fee2e2;
color: #b91c1c;
border: 1px solid #fecaca;
&--yes {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
.racc-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
&--from {
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
}
}
.racc-alert--editable {
border-color: #bae6fd;
background: #f0f9ff;
&:hover {
border-color: #7dd3fc;
box-shadow: 0 2px 8px rgb(2 132 199 / 10%);
}
h4 {
color: #0369a1;
}
}
@@ -0,0 +1,60 @@
/**
* 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, Input, OnChanges, SimpleChanges } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component';
import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'mifosx-recurring-deposits-account-currency-step',
templateUrl: './recurring-deposits-account-currency-step.component.html',
styleUrls: ['./recurring-deposits-account-currency-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
NomineeFormComponent,
DormancyAlertPanelComponent,
MatSlideToggle,
FormsModule
]
})
export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges {
@Input() recurringDepositsAccountProductTemplate: any;
nomineeData: any = null;
// Editable feature toggles (pre-filled from product, overridable by user)
balanceSwapOption = false;
msme = false;
autoDebitFacility = false;
ngOnChanges(changes: SimpleChanges): void {
if (changes['recurringDepositsAccountProductTemplate']?.currentValue) {
const pt = changes['recurringDepositsAccountProductTemplate'].currentValue;
this.balanceSwapOption = !!pt.balanceSwapOption;
this.msme = !!pt.msme;
this.autoDebitFacility = !!pt.autoDebitFacility;
}
}
onNomineeChange(event: any): void {
this.nomineeData = event;
}
/** Called by parent to collect currency step values */
get currencyStepData() {
return {
balanceSwapOption: this.balanceSwapOption,
msme: this.msme,
autoDebitFacility: this.autoDebitFacility,
...(this.nomineeData ? { nominee: this.nomineeData } : {})
};
}
}
@@ -7,7 +7,7 @@
-->
<form [formGroup]="recurringDepositAccountDetailsForm">
<!-- ===================== Account Details ===================== -->
<!-- ══ Account Details ══════════════════════════════════════════ -->
<div class="rad-section">
<div class="rad-section__header">
<i class="fas fa-info-circle"></i>
@@ -64,7 +64,7 @@
</div>
@if (isProductSelected && productTemplate) {
<!-- ===================== Product Classification ===================== -->
<!-- ══ Product Classification (read-only) ══════════════════════ -->
<div class="rad-section rad-section--mt">
<div class="rad-section__header">
<i class="fas fa-tags"></i>
@@ -115,101 +115,8 @@
</div>
</div>
<!-- ===================== Currency & Account Status Configuration ===================== -->
<div class="rad-section rad-section--mt">
<div class="rad-section__header">
<i class="fas fa-coins"></i>
<h3>Currency &amp; Account Status Configuration</h3>
<span class="rad-badge">From Product</span>
</div>
<div class="rad-body">
@if (productTemplate.currency) {
<div class="rad-grid">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Currency</mat-label>
<input matInput [value]="productTemplate.currency.name" readonly />
<mat-hint>Default: Indian Rupee - auto-set for domestic products</mat-hint>
</mat-form-field>
@if (
productTemplate.currency.decimalPlaces !== undefined && productTemplate.currency.decimalPlaces !== null
) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Decimal Places</mat-label>
<input matInput [value]="productTemplate.currency.decimalPlaces" readonly />
</mat-form-field>
}
</div>
}
<div class="rad-alert">
<div class="rad-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.dormancyAlert === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.dormancyAlert === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.balanceSwapOption === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.balanceSwapOption === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.msme === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.msme === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-alert__content">
<h4>Nomination Facility</h4>
<p>
Allow account holders to designate individuals to receive funds in the event of account holder's death.
</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.nominationFacility === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.nominationFacility === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.autoDebitFacility === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.autoDebitFacility === false">no</span>
</div>
</div>
</div>
</div>
@if (productTemplate?.introducerRequired) {
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
}
}
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="recurringDepositAccountDetailsForm.invalid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</form>
@@ -16,7 +16,7 @@ $gray-900: #202124;
.rad-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
overflow: visible;
background: #fff;
&--mt {
@@ -183,3 +183,61 @@ $gray-900: #202124;
}
}
}
// ── Account Feature Toggles ────────────────────────────────────────────────
.rad-toggles {
display: flex;
flex-direction: column;
gap: 10px;
padding: 4px 0;
}
.rad-toggle-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
background: #fff;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:hover {
border-color: #93c5fd;
box-shadow: 0 1px 6px rgb(59 130 246 / 10%);
}
&__left {
display: flex;
align-items: center;
gap: 12px;
> i {
width: 36px;
height: 36px;
border-radius: 8px;
background: #f0f9ff;
border: 1px solid #bae6fd;
color: #0284c7;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
flex: 0 0 auto;
}
}
&__label {
font-size: 13.5px;
font-weight: 600;
color: #1e293b;
}
&__hint {
font-size: 11.5px;
color: #64748b;
margin-top: 2px;
}
}
@@ -18,6 +18,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
/**
* Recurring Deposits Account Details Step
@@ -31,7 +32,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
MatStepperNext,
IntroducerSearchComponent
]
})
export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
@@ -58,6 +60,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
recurringDepositAccountDetailsForm!: UntypedFormGroup;
isProductSelected = false;
introducerId: number | null = null;
/** Recurring Deposits Account Template with product data */
@Output() recurringDepositsAccountProductTemplate = new EventEmitter();
@@ -106,7 +109,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
Validators.required
],
fieldOfficerId: [''],
externalId: ['']
externalId: [''],
balanceSwapOption: [false],
msme: [false],
autoDebitFacility: [false]
});
}
@@ -135,6 +141,12 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.recurringDepositsAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.isProductSelected = true;
// Pre-populate toggles from product defaults (user can override)
this.recurringDepositAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
autoDebitFacility: !!response.autoDebitFacility
});
this.cdr.detectChanges();
if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) {
this.recurringDepositAccountDetailsForm
@@ -184,7 +196,15 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
/**
* Returns recurring deposits account details form value.
*/
onIntroducerChange(event: { clientId: number; displayName: string } | null): void {
this.introducerId = event?.clientId ?? null;
}
get recurringDepositAccountDetails() {
return this.recurringDepositAccountDetailsForm.value;
const value = this.recurringDepositAccountDetailsForm.value;
if (this.introducerId) {
value.introducerId = this.introducerId;
}
return value;
}
}
@@ -207,14 +207,3 @@
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
@@ -249,316 +249,6 @@
</div>
</div>
<!-- ===================== Terms ===================== -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Terms' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Nominal Annual Interest' | translate }}</span>
<span class="preview-value">
{{ recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? '—' }}
@if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate != null) {
%
}
</span>
</div>
@if (recurringDepositsAccountProductTemplate?.effectiveDate) {
<div class="preview-field">
<span class="preview-label">Effective Date</span>
<span class="preview-value">{{ recurringDepositsAccountProductTemplate?.effectiveDate | dateFormat }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Compounding Period' | translate }}</span>
<span class="preview-value">{{
recurringDepositAccountData?.interestCompoundingPeriodType
| find: recurringDepositsAccountProductTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Posting Period' | translate }}</span>
<span class="preview-value">{{
recurringDepositAccountData?.interestPostingPeriodType
| find: recurringDepositsAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
@if (recurringDepositsAccountProductTemplate?.averagePeriod) {
<div class="preview-field">
<span class="preview-label">Average Period</span>
<span class="preview-value">{{ recurringDepositsAccountProductTemplate?.averagePeriod }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Calculated using' | translate }}</span>
<span class="preview-value">{{
recurringDepositAccountData?.interestCalculationType
| find: recurringDepositsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Days in Year' | translate }}</span>
<span class="preview-value">{{
recurringDepositAccountData?.interestCalculationDaysInYearType
| find: recurringDepositsAccountProductTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
@if (
recurringDepositsAccountProductTemplate?.autoPost !== undefined &&
recurringDepositsAccountProductTemplate?.autoPost !== null
) {
<div class="preview-field">
<span class="preview-label">Auto Interest Post</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositsAccountProductTemplate?.autoPost"
>
{{ recurringDepositsAccountProductTemplate?.autoPost | yesNo }}
</span>
</div>
}
@if (
recurringDepositsAccountProductTemplate?.frequencyFixDay !== undefined &&
recurringDepositsAccountProductTemplate?.frequencyFixDay !== null
) {
<div class="preview-field">
<span class="preview-label">Frequency (Fix Day)</span>
<span class="preview-value">{{ recurringDepositsAccountProductTemplate?.frequencyFixDay }}</span>
</div>
}
@if (recurringDepositsAccountProductTemplate?.lastInterestDate) {
<div class="preview-field">
<span class="preview-label">Last Interest Date</span>
<span class="preview-value">{{
recurringDepositsAccountProductTemplate?.lastInterestDate | dateFormat
}}</span>
</div>
}
@if (recurringDepositsAccountProductTemplate?.interestCalculationTypeName) {
<div class="preview-field">
<span class="preview-label">Interest Calculation Type</span>
<span class="preview-value">{{
recurringDepositsAccountProductTemplate?.interestCalculationTypeName
}}</span>
</div>
}
</div>
</div>
</div>
<!-- ===================== Settings ===================== -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Settings' | translate }}</span>
</div>
<div class="preview-body">
<!-- Premature Option -->
<div class="preview-subsection">
<div class="preview-subsection__title">Premature Option</div>
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Loan Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.loanFacility"
>
{{ recurringDepositAccountData?.loanFacility | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Premature Withdrawal Option</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.prematureWithdrawalOption"
>
{{ recurringDepositAccountData?.prematureWithdrawalOption | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Tenure Flexibility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.tenureFlexibility"
>
{{ recurringDepositAccountData?.tenureFlexibility | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Lien Marked</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.lienMarked"
>
{{ recurringDepositAccountData?.lienMarked | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Withdrawal Fee for Transfers</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.withdrawalFeeForTransfers"
>
{{ recurringDepositAccountData?.withdrawalFeeForTransfers | yesNo }}
</span>
</div>
</div>
</div>
<mat-divider class="preview-divider"></mat-divider>
<!-- Lock-in Period -->
<div class="preview-subsection">
<div class="preview-subsection__title">{{ 'labels.inputs.Lock-in Period' | translate }}</div>
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Enable Lock-in Period</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.enableLockinPeriod"
>
{{ recurringDepositAccountData?.enableLockinPeriod | yesNo }}
</span>
</div>
@if (recurringDepositAccountData?.enableLockinPeriod) {
<div class="preview-field">
<span class="preview-label">Lock-in Frequency</span>
<span class="preview-value">
{{ recurringDepositAccountData?.lockinPeriodFrequency }}
{{
recurringDepositAccountData?.lockinPeriodFrequencyType
| find: recurringDepositsAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}
</span>
</div>
}
</div>
</div>
<mat-divider class="preview-divider"></mat-divider>
<!-- Maturity Instructions -->
<div class="preview-subsection">
<div class="preview-subsection__title">Maturity Instructions</div>
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Maturity Instructions</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.maturityInstructions"
>
{{ recurringDepositAccountData?.maturityInstructions | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Credit Maturity Proceeds</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.creditMaturityProceeds"
>
{{ recurringDepositAccountData?.creditMaturityProceeds | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Penalty / Grace Period for Missed Installments</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.penaltyGracePeriodMissedInstallments"
>
{{ recurringDepositAccountData?.penaltyGracePeriodMissedInstallments | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Panel Request Applicable</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.panelRequestApplicable"
>
{{ recurringDepositAccountData?.panelRequestApplicable | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">No Penalty for Nominee / Legal Heir Closure</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.noPenaltyNomineeHeirClosure"
>
{{ recurringDepositAccountData?.noPenaltyNomineeHeirClosure | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Deceased Settlement</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.deceasedSettlement"
>
{{ recurringDepositAccountData?.deceasedSettlement | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Allow Debit Txn after Due Date</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.allowDebitTxnAfterDueDate"
>
{{ recurringDepositAccountData?.allowDebitTxnAfterDueDate | yesNo }}
</span>
</div>
</div>
</div>
<mat-divider class="preview-divider"></mat-divider>
<!-- Additional Rules -->
<div class="preview-subsection">
<div class="preview-subsection__title">Additional Rules</div>
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Account Closed with Zero Balance in Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="recurringDepositAccountData?.accountClosedZeroBalanceInDayClosing"
>
{{ recurringDepositAccountData?.accountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
@if (recurringDepositAccountData?.minBalanceForInterestCalculation) {
<div class="preview-field">
<span class="preview-label">Min. Balance for Interest Calculation</span>
<span class="preview-value">{{
recurringDepositAccountData?.minBalanceForInterestCalculation | formatNumber
}}</span>
</div>
}
@if (recurringDepositAccountData?.mandatoryRecommendedDepositAmount) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Recurring Deposit Amount' | translate }}</span>
<span class="preview-value">{{
recurringDepositAccountData?.mandatoryRecommendedDepositAmount | formatNumber
}}</span>
</div>
}
</div>
</div>
</div>
</div>
<!-- ===================== Interest Rate Chart ===================== -->
@if (_activeRdVersion || recurringDepositsAccountProductTemplate?.accountChart) {
<div class="preview-section">
@@ -770,82 +460,6 @@
</div>
</div>
}
<!-- ===================== Charges ===================== -->
@if (recurringDepositAccountData?.charges?.length) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Charges' | translate }}</span>
</div>
<div class="preview-body">
<table
class="flex-100 mat-elevation-z1 preview-table"
mat-table
[dataSource]="recurringDepositAccountData.charges"
>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name + ', ' + charge.currency?.displaySymbol }}</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType?.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType?.value }}</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<span
*ngIf="
charge.chargeTimeType?.value === 'Specified due date' || charge.chargeTimeType?.value === 'Weekly Fee'
"
>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
<span
*ngIf="charge.chargeTimeType?.value === 'Monthly Fee' || charge.chargeTimeType?.value === 'Annual Fee'"
>
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}
</span>
<span
*ngIf="
!(
charge.chargeTimeType?.value === 'Monthly Fee' ||
charge.chargeTimeType?.value === 'Annual Fee' ||
charge.chargeTimeType?.value === 'Specified due date' ||
charge.chargeTimeType?.value === 'Weekly Fee'
)
"
>
{{ 'labels.inputs.N/A' | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.feeInterval || 'Not Provided' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
</div>
}
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
@@ -51,6 +51,10 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
this.recurringDepositProductsTemplate.currency?.decimalPlaces ??
2,
dormancyAlert: this.recurringDepositProductsTemplate.dormancyAlert ?? false,
isDormancyTrackingActive: this.recurringDepositProductsTemplate.isDormancyTrackingActive ?? false,
daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30,
daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60,
daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90,
balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false,
msme: this.recurringDepositProductsTemplate.msme ?? false,
nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false,
@@ -72,6 +76,10 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
]
],
dormancyAlert: [false],
isDormancyTrackingActive: [false],
daysToInactive: [30],
daysToDormancy: [60],
daysToEscheat: [90],
balanceSwapOption: [false],
msme: [false],
nominationFacility: [false],
@@ -81,7 +89,7 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
get recurringDepositProductCurrency() {
const formValue = this.recurringDepositProductCurrencyForm.value;
return {
const result: any = {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
@@ -90,5 +98,11 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
};
if (formValue.dormancyAlert) {
result.daysToInactive = formValue.daysToInactive;
result.daysToDormancy = formValue.daysToDormancy;
result.daysToEscheat = formValue.daysToEscheat;
}
return result;
}
}
@@ -35,7 +35,7 @@
</ng-template>
<!-- Step 1: Product Classification + Details -->
<mat-step [stepControl]="savingsAccountDetailsForm">
<mat-step>
<ng-template matStepLabel>Product Classification</ng-template>
<mifosx-savings-account-details-step
@@ -45,96 +45,46 @@
</mifosx-savings-account-details-step>
</mat-step>
<!-- Step 2: Currency -->
<mat-step>
<ng-template matStepLabel>Currency</ng-template>
@if (savingsAccountProductTemplate) {
<!-- Step 2: Currency -->
<mat-step>
<ng-template matStepLabel>Currency</ng-template>
<mifosx-savings-account-currency-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-currency-step>
<mifosx-savings-account-currency-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-currency-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 3: Term -->
<mat-step [stepControl]="savingsAccountTermsForm">
<ng-template matStepLabel>Term</ng-template>
<!-- Step 3: Digital -->
<mat-step>
<ng-template matStepLabel>Digital</ng-template>
<mifosx-savings-account-term-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-term-step>
<mifosx-savings-account-digital-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-digital-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="savingsAccountTermsForm.invalid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 4: Settings -->
<mat-step [stepControl]="savingsAccountSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-savings-account-settings-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-settings-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 5: Digital -->
<mat-step>
<ng-template matStepLabel>Digital</ng-template>
<mifosx-savings-account-digital-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-digital-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 6: Charges -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-savings-account-charges-step
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
[savingsAccountTemplate]="savingsAccountTemplate"
[currencyCode]="savingsAccountTermsForm.get('currencyCode')"
>
</mifosx-savings-account-charges-step>
</mat-step>
<!-- Step 7: Preview -->
@if (savingsAccountFormValid) {
<!-- Step 4: Preview -->
<mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-savings-account-preview-step
@@ -27,11 +27,14 @@
.mat-step-content {
padding: 1.5rem;
margin: 1rem 0;
overflow: visible !important;
}
}
// Form field spacing within steps
:host ::ng-deep mat-step-content {
overflow: visible !important;
mat-form-field {
margin-bottom: 1rem;
width: 100%;
@@ -9,12 +9,10 @@
/** Angular Imports */
import { Component, ViewChild, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormGroup } from '@angular/forms';
/** Custom Components */
import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component';
import { SavingsAccountTermStepComponent } from '../savings-account-stepper/savings-account-term-step/savings-account-term-step.component';
import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component';
import { SavingsAccountChargesStepComponent } from '../savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component';
import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component';
import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component';
@@ -22,7 +20,14 @@ import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/s
import { SavingsService } from '../savings.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import {
MatStepper,
MatStepperIcon,
MatStep,
MatStepLabel,
MatStepperNext,
MatStepperPrevious
} from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { SavingsAccountPreviewStepComponent } from '../savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -38,14 +43,13 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
MatStepperNext,
MatStepperPrevious,
FaIconComponent,
MatStep,
MatStepLabel,
SavingsAccountDetailsStepComponent,
SavingsAccountCurrencyStepComponent,
SavingsAccountTermStepComponent,
SavingsAccountSettingsStepComponent,
SavingsAccountChargesStepComponent,
SavingsAccountDigitalStepComponent,
SavingsAccountPreviewStepComponent
]
@@ -65,15 +69,6 @@ export class CreateSavingsAccountComponent {
@ViewChild(SavingsAccountDetailsStepComponent, { static: true })
savingsAccountDetailsStep: SavingsAccountDetailsStepComponent;
@ViewChild(SavingsAccountTermStepComponent, { static: true })
savingsAccountTermStep: SavingsAccountTermStepComponent;
@ViewChild(SavingsAccountSettingsStepComponent, { static: true })
savingsAccountSettingsStep: SavingsAccountSettingsStepComponent;
@ViewChild(SavingsAccountChargesStepComponent, { static: true })
savingsAccountChargesStep: SavingsAccountChargesStepComponent;
constructor() {
this.route.data.subscribe((data: { savingsAccountTemplate: any }) => {
this.savingsAccountTemplate = data.savingsAccountTemplate;
@@ -88,28 +83,14 @@ export class CreateSavingsAccountComponent {
return this.savingsAccountDetailsStep.savingsAccountDetailsForm;
}
get savingsAccountTermsForm() {
return this.savingsAccountTermStep.savingsAccountTermForm;
}
get savingsAccountSettingsForm() {
return this.savingsAccountSettingsStep.savingsAccountSettingsForm;
}
get savingsAccountFormValid() {
return (
this.savingsAccountDetailsForm.valid &&
this.savingsAccountTermsForm.valid &&
this.savingsAccountSettingsForm.valid
);
/** Stub — no term step in this flow */
get savingsAccountTermsForm(): FormGroup {
return new FormGroup({});
}
get savingsAccount() {
return {
...this.savingsAccountDetailsStep.savingsAccountDetails,
...this.savingsAccountTermStep.savingsAccountTerms,
...this.savingsAccountSettingsStep.savingsAccountSettings,
...this.savingsAccountChargesStep.savingsAccountCharges
...this.savingsAccountDetailsStep.savingsAccountDetails
};
}
@@ -119,15 +100,7 @@ export class CreateSavingsAccountComponent {
const monthDayFormat = 'dd MMMM';
const savingsAccount = {
...this.savingsAccount,
charges: this.savingsAccount.charges.map((charge: any) => ({
chargeId: charge.id,
amount: charge.amount,
dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate,
feeOnMonthDay: charge.feeOnMonthDay
? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat)
: charge.feeOnMonthDay,
feeInterval: charge.feeInterval
})),
charges: [],
submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat),
dateFormat,
monthDayFormat,
@@ -36,6 +36,8 @@
</span>
</div>
<mifosx-dormancy-alert-panel [productTemplate]="savingsAccountProductTemplate"></mifosx-dormancy-alert-panel>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Nomination Facility</h4>
@@ -45,5 +47,9 @@
{{ savingsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
</span>
</div>
@if (savingsAccountProductTemplate?.nominationFacility) {
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
}
</div>
</div>
@@ -8,13 +8,25 @@
import { Component, Input } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component';
import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component';
@Component({
selector: 'mifosx-savings-account-currency-step',
templateUrl: './savings-account-currency-step.component.html',
styleUrls: ['./savings-account-currency-step.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
imports: [
...STANDALONE_SHARED_IMPORTS,
NomineeFormComponent,
DormancyAlertPanelComponent
]
})
export class SavingsAccountCurrencyStepComponent {
@Input() savingsAccountProductTemplate: any;
nomineeData: any = null;
onNomineeChange(event: any): void {
this.nomineeData = event;
}
}
@@ -67,7 +67,7 @@
</div>
</div>
<!-- Product Classification (read-only, shown after product is selected) -->
<!-- Product Classification + Introducer (shown after product is selected) -->
@if (savingsProductSelected) {
<div class="sad-section sad-section--mt">
<div class="sad-section__header">
@@ -118,16 +118,9 @@
</div>
</div>
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="savingsAccountDetailsForm.invalid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
@if (productTemplate?.introducerRequired) {
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
}
}
</form>
@@ -15,7 +15,7 @@ $gray-900: #202124;
.sad-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
overflow: visible;
background: #fff;
&--mt {
@@ -17,6 +17,7 @@ import { SavingsService } from 'app/savings/savings.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
/**
* Savings Account Details Step
@@ -29,7 +30,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
MatStepperNext,
IntroducerSearchComponent
]
})
export class SavingsAccountDetailsStepComponent implements OnInit {
@@ -56,6 +58,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
savingsAccountDetailsForm: UntypedFormGroup;
savingsProductSelected = false;
introducerId: number | null = null;
/** Savings Account Template with product data */
@Output() savingsAccountProductTemplate = new EventEmitter();
@@ -165,7 +168,15 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
/**
* Returns savings account form value.
*/
onIntroducerChange(event: { clientId: number; displayName: string } | null): void {
this.introducerId = event?.clientId ?? null;
}
get savingsAccountDetails() {
return this.savingsAccountDetailsForm.getRawValue();
const value = this.savingsAccountDetailsForm.getRawValue();
if (this.introducerId) {
value.introducerId = this.introducerId;
}
return value;
}
}
@@ -6,229 +6,209 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="spl-section">
<div class="spl-section__header">
<span class="spl-section__icon">
<i class="fas fa-chart-bar"></i>
</span>
<div class="spl-section__title">Transaction &amp; Product Limits</div>
<div class="spl-section__badge">FROM PRODUCT</div>
<div class="dig-wrapper">
<div class="dig-header">
<div class="dig-header__icon">
<i class="fas fa-mobile-alt"></i>
</div>
<div class="dig-header__text">
<h3 class="dig-header__title">Digital &amp; Account Features</h3>
<p class="dig-header__sub">Toggle the facilities available for this account (as per RBI guidelines)</p>
</div>
<span class="dig-header__badge">FROM PRODUCT</span>
</div>
<div class="spl-section__body">
<div class="spl-features">
<div class="spl-features__header">Account Features (as per RBI guidelines)</div>
<div class="spl-features__grid">
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-automobile"></i> ATM Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.atmFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-wallet"></i> ATM Withdrawal</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.atmWithdrawalFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-mobile-alt"></i> UPI Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.upiFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-exchange-alt"></i> POS Transactions</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.posTransactionsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-book"></i> Cheque Book</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.chequeBook" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-bolt"></i> IMPS Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.impsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-sms"></i> SMS Alerts</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.smsAlerts" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-bell"></i> BBPS Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.bbpsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-shield-alt"></i> Insurance Cover</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.insuranceCover" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-bolt"></i> BEPG Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.bepgFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-money-check-alt"></i> NEFT Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.neftFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-money-check"></i> RTGS Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.rtgsFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-balance-scale"></i> Inter-Bank Total Cap</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.interBankTotalCapFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-arrow-right-arrow-left"></i> Withdrawal Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.withdrawalFacility" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-id-card"></i> Deposit Facility (KYC)</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.depositFacilityKyc" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<label class="spl-feature">
<span class="spl-feature__label"><i class="fas fa-credit-card"></i> Virtual Debit Card</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.virtualDebitCard" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
<div class="dig-grid">
<div class="dig-card" [class.dig-card--on]="facilityConfig.atmFacility">
<div class="dig-card__icon"><i class="fas fa-credit-card"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">ATM Facility</span>
<span class="dig-card__status">{{ facilityConfig.atmFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.atmFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
@if (accountFacilityLimits.length > 0) {
<div class="spl-table-wrap">
<table class="spl-table">
<colgroup>
<col class="spl-table__col--label" />
<col class="spl-table__col--value" />
<col class="spl-table__col--value" />
<col class="spl-table__col--value" />
</colgroup>
<thead>
<tr>
<th>Limit Type</th>
<th>General Limit (Daily)</th>
<th>Minor Account Limit</th>
<th>Small Saving Limit</th>
</tr>
</thead>
<tbody>
@for (row of accountFacilityLimits; track row.limitType) {
<tr>
<td>{{ row.limitType }}</td>
<td><input type="text" class="spl-input" [value]="row.generalLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.minorAccountLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.smallSavingLimit" disabled /></td>
</tr>
}
</tbody>
</table>
<div class="dig-card" [class.dig-card--on]="facilityConfig.atmWithdrawalFacility">
<div class="dig-card__icon"><i class="fas fa-wallet"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">ATM Withdrawal</span>
<span class="dig-card__status">{{ facilityConfig.atmWithdrawalFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
}
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.atmWithdrawalFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
@if (depositLimitKyc?.general) {
<div class="spl-table-wrap spl-table-wrap--kyc">
<h4 class="mat-h4">Deposit Limit (KYC)</h4>
<table class="spl-table">
<thead>
<tr>
<th>Account Type</th>
<th>KYC Type</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.general.kycType" disabled>
<option>{{ depositLimitKyc.general.kycType }}</option>
</select>
</td>
<td><input type="text" class="spl-input" [value]="depositLimitKyc.general.limit" disabled /></td>
</tr>
<tr>
<td>Minor</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.minor.kycType" disabled>
<option>{{ depositLimitKyc.minor.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.minor.limit" disabled /></td>
</tr>
<tr>
<td>Small Saving</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.smallSaving.kycType" disabled>
<option>{{ depositLimitKyc.smallSaving.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.smallSaving.limit" disabled /></td>
</tr>
</tbody>
</table>
<div class="dig-card" [class.dig-card--on]="facilityConfig.upiFacility">
<div class="dig-card__icon"><i class="fas fa-mobile-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">UPI Facility</span>
<span class="dig-card__status">{{ facilityConfig.upiFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
}
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.upiFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="spl-note">
<strong>Note:</strong>
These facilities are defined at product level and applied to this account. Small Saving Account limits are
strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular.
<div class="dig-card" [class.dig-card--on]="facilityConfig.posTransactionsFacility">
<div class="dig-card__icon"><i class="fas fa-exchange-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">POS Transactions</span>
<span class="dig-card__status">{{ facilityConfig.posTransactionsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.posTransactionsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.chequeBook">
<div class="dig-card__icon"><i class="fas fa-book"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Cheque Book</span>
<span class="dig-card__status">{{ facilityConfig.chequeBook ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.chequeBook" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.impsFacility">
<div class="dig-card__icon"><i class="fas fa-bolt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">IMPS Facility</span>
<span class="dig-card__status">{{ facilityConfig.impsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.impsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.smsAlerts">
<div class="dig-card__icon"><i class="fas fa-sms"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">SMS Alerts</span>
<span class="dig-card__status">{{ facilityConfig.smsAlerts ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.smsAlerts" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.bbpsFacility">
<div class="dig-card__icon"><i class="fas fa-bell"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">BBPS Facility</span>
<span class="dig-card__status">{{ facilityConfig.bbpsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.bbpsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.insuranceCover">
<div class="dig-card__icon"><i class="fas fa-shield-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Insurance Cover</span>
<span class="dig-card__status">{{ facilityConfig.insuranceCover ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.insuranceCover" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.bepgFacility">
<div class="dig-card__icon"><i class="fas fa-plug"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">BEPG Facility</span>
<span class="dig-card__status">{{ facilityConfig.bepgFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.bepgFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.neftFacility">
<div class="dig-card__icon"><i class="fas fa-money-check-alt"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">NEFT Facility</span>
<span class="dig-card__status">{{ facilityConfig.neftFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.neftFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.rtgsFacility">
<div class="dig-card__icon"><i class="fas fa-money-check"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">RTGS Facility</span>
<span class="dig-card__status">{{ facilityConfig.rtgsFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.rtgsFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.interBankTotalCapFacility">
<div class="dig-card__icon"><i class="fas fa-balance-scale"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Inter-Bank Total Cap</span>
<span class="dig-card__status">{{ facilityConfig.interBankTotalCapFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.interBankTotalCapFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.withdrawalFacility">
<div class="dig-card__icon"><i class="fas fa-arrow-right-arrow-left"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Withdrawal Facility</span>
<span class="dig-card__status">{{ facilityConfig.withdrawalFacility ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.withdrawalFacility" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.depositFacilityKyc">
<div class="dig-card__icon"><i class="fas fa-id-card"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Deposit Facility (KYC)</span>
<span class="dig-card__status">{{ facilityConfig.depositFacilityKyc ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.depositFacilityKyc" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
<div class="dig-card" [class.dig-card--on]="facilityConfig.virtualDebitCard">
<div class="dig-card__icon"><i class="fas fa-credit-card"></i></div>
<div class="dig-card__body">
<span class="dig-card__label">Virtual Debit Card</span>
<span class="dig-card__status">{{ facilityConfig.virtualDebitCard ? 'Enabled' : 'Disabled' }}</span>
</div>
<label class="dig-toggle">
<input type="checkbox" [(ngModel)]="facilityConfig.virtualDebitCard" />
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
</label>
</div>
</div>
</div>
@@ -6,194 +6,205 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$border: #d9e2ec;
$border-strong: #c7d2de;
$text: #24364b;
$blue: #2563eb;
$blue-light: #eff6ff;
$green: #16a34a;
$green-light: #f0fdf4;
$green-border: #bbf7d0;
$border: #e2e8f0;
$text: #1e293b;
$muted: #64748b;
$header-bg: #f8fafc;
$gray-bg: #f8fafc;
.spl-section {
// ── Wrapper ──────────────────────────────────────────────
.dig-wrapper {
border: 1px solid $border;
border-radius: 12px;
border-radius: 14px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
}
.spl-section__header {
// ── Header ───────────────────────────────────────────────
.dig-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: $header-bg;
gap: 14px;
padding: 18px 24px;
background: $gray-bg;
border-bottom: 1px solid $border;
}
.spl-section__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 8px;
background: #eff6ff;
color: #2563eb;
font-size: 14px;
flex: 0 0 auto;
}
.spl-section__title {
flex: 1 1 auto;
font-size: 16px;
font-weight: 600;
color: $text;
}
.spl-section__badge {
flex: 0 0 auto;
padding: 5px 10px;
border-radius: 999px;
background: #dbeafe;
color: #1d4ed8;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
white-space: nowrap;
}
.spl-section__body {
padding: 18px 20px 20px;
}
.spl-table-wrap {
overflow-x: auto;
margin-top: 16px;
}
.spl-table {
width: 100%;
min-width: 0;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid $border;
col.spl-table__col--label {
width: 22%;
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
background: $blue-light;
color: $blue;
font-size: 16px;
flex: 0 0 auto;
}
col.spl-table__col--value {
width: 26%;
&__text {
flex: 1;
}
th,
td {
border: 1px solid $border;
padding: 6px 8px;
vertical-align: middle;
}
th {
background: #f8fafc;
color: #64748b;
text-align: center;
font-size: 11px;
&__title {
margin: 0 0 2px;
font-size: 15px;
font-weight: 700;
}
td:first-child {
background: #f8fafc;
font-weight: 600;
color: $text;
white-space: nowrap;
}
td {
font-size: 13px;
overflow: hidden;
&__sub {
margin: 0;
font-size: 12px;
color: $muted;
}
&__badge {
flex: 0 0 auto;
padding: 4px 12px;
border-radius: 999px;
background: #dbeafe;
color: $blue;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
}
}
.spl-input,
.spl-select {
width: 100%;
box-sizing: border-box;
font: inherit;
}
.spl-input {
height: 30px;
border: 1px solid $border-strong;
border-radius: 2px;
padding: 0 8px;
background: #f8fafc;
color: $muted;
}
.spl-select {
height: 30px;
border: 1px solid $border-strong;
border-radius: 2px;
padding: 0 6px;
background: #f8fafc;
color: $muted;
}
.spl-features {
margin-bottom: 16px;
}
.spl-features__header {
font-size: 13px;
font-weight: 600;
color: $muted;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.spl-features__grid {
// ── Grid ─────────────────────────────────────────────────
.dig-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 16px;
grid-template-columns: repeat(3, 1fr);
gap: 0;
@media (width <= 900px) {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 960px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 640px) {
@media (width <= 580px) {
grid-template-columns: 1fr;
}
}
.spl-feature {
// ── Card ─────────────────────────────────────────────────
.dig-card {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
color: $text;
}
.spl-feature__label {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #334155;
gap: 12px;
padding: 14px 18px;
border-right: 1px solid $border;
border-bottom: 1px solid $border;
background: #fff;
transition: background 0.18s ease;
cursor: pointer;
i {
color: #2563eb;
font-size: 12px;
width: 14px;
text-align: center;
&:nth-child(3n) {
border-right: none;
}
&--on {
background: $green-light;
.dig-card__icon {
background: $green-border;
color: $green;
}
.dig-card__status {
color: $green;
font-weight: 600;
}
}
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 9px;
background: $blue-light;
color: $blue;
font-size: 14px;
flex: 0 0 auto;
transition:
background 0.18s,
color 0.18s;
}
&__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
&__label {
font-size: 13px;
font-weight: 600;
color: $text;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__status {
font-size: 11px;
color: $muted;
transition: color 0.18s;
}
}
.spl-note {
margin-top: 16px;
padding: 12px 14px;
border: 1px solid $border;
border-radius: 8px;
background: #f8fafc;
color: $muted;
font-size: 11px;
line-height: 1.6;
// ── Toggle Switch ─────────────────────────────────────────
.dig-toggle {
position: relative;
flex: 0 0 auto;
cursor: pointer;
input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
&:checked + .dig-toggle__track {
background: $green;
border-color: $green;
}
&:checked + .dig-toggle__track .dig-toggle__thumb {
transform: translateX(18px);
}
}
&__track {
display: block;
width: 40px;
height: 22px;
border-radius: 999px;
background: #cbd5e1;
border: 1.5px solid #94a3b8;
position: relative;
transition:
background 0.2s,
border-color 0.2s;
}
&__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 20%);
transition: transform 0.2s ease;
}
}
@@ -39,31 +39,16 @@ const DEFAULT_FACILITY_CONFIG = {
]
})
export class SavingsAccountDigitalStepComponent {
private _savingsAccountProductTemplate: any;
facilityConfig = { ...DEFAULT_FACILITY_CONFIG };
@Input()
set savingsAccountProductTemplate(value: any) {
this._savingsAccountProductTemplate = value;
}
get savingsAccountProductTemplate(): any {
return this._savingsAccountProductTemplate;
if (value?.accountFacilityConfig) {
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
}
}
get accountFacilityConfig() {
return this._savingsAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG;
}
get accountFacilityLimits(): any[] {
return this._savingsAccountProductTemplate?.accountFacilityLimits || [];
}
get depositLimitKyc() {
return (
this._savingsAccountProductTemplate?.depositLimitKyc || {
general: { kycType: '', limit: '' },
minor: { kycType: '', limit: '' },
smallSaving: { kycType: '', limit: '' }
}
);
get savingsAccountDigital() {
return { accountFacilityConfig: { ...this.facilityConfig } };
}
}
@@ -213,198 +213,6 @@
</div>
</div>
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Terms' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Nominal Annual Interest' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
@if (savingsAccountProductTemplate?.interestCalculationTypeName) {
<div class="preview-field">
<span class="preview-label">Interest Calculation Mode</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.interestCalculationTypeName }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Compounding Period' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestCompoundingPeriodType
| find: savingsAccountProductTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Posting Period' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestPostingPeriodType
| find: savingsAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Calculated using' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestCalculationType
| find: savingsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Days in Year' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestCalculationDaysInYearType
| find: savingsAccountProductTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">Interest Calculation Selector</span>
<span class="preview-value">{{
savingsAccount?.interestCalculateSelector
| find: savingsAccountProductTemplate?.interestCalculationTypeOption : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
@if (savingsAccount?.averagePeriod) {
<div class="preview-field">
<span class="preview-label">Average Period</span>
<span class="preview-value">{{ savingsAccount?.averagePeriod }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Auto Post</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="savingsAccount?.autoPost">
{{ savingsAccount?.autoPost | yesNo }}
</span>
</div>
@if (savingsAccountProductTemplate?.lastInterestDate) {
<div class="preview-field">
<span class="preview-label">Last Interest Date</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.lastInterestDate | dateFormat }}</span>
</div>
}
@if (savingsAccount?.frequencyFixDay !== undefined && savingsAccount?.frequencyFixDay !== null) {
<div class="preview-field">
<span class="preview-label">Frequency Fix Day</span>
<span class="preview-value">{{ savingsAccount?.frequencyFixDay }}</span>
</div>
}
</div>
</div>
</div>
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Settings' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (savingsAccount?.minRequiredOpeningBalance) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Opening Balance' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccount?.withdrawalFeeForTransfers"
>
{{ savingsAccount?.withdrawalFeeForTransfers | yesNo }}
</span>
</div>
@if (savingsAccount?.lockinPeriodFrequency) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Lock-in Period' | translate }}</span>
<span class="preview-value">
{{ savingsAccount?.lockinPeriodFrequency }}
{{
savingsAccount?.lockinPeriodFrequencyType
| find: savingsAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}
</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="savingsAccount?.allowOverdraft">
{{ savingsAccount?.allowOverdraft | yesNo }}
</span>
</div>
@if (savingsAccount?.minOverdraftForInterestCalculation) {
<div class="preview-field">
<span class="preview-label">{{
'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate
}}</span>
<span class="preview-value">{{ savingsAccount?.minOverdraftForInterestCalculation | formatNumber }}</span>
</div>
}
@if (savingsAccount?.nominalAnnualInterestRateOverdraft) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
</div>
}
@if (savingsAccount?.overdraftLimit) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.overdraftLimit | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccount?.enforceMinRequiredBalance"
>
{{ savingsAccount?.enforceMinRequiredBalance | yesNo }}
</span>
</div>
@if (savingsAccountProductTemplate?.enableLockinPeriod !== undefined) {
<div class="preview-field">
<span class="preview-label">Enable Lock-in Period</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.enableLockinPeriod"
>
{{ savingsAccountProductTemplate?.enableLockinPeriod | yesNo }}
</span>
</div>
}
@if (savingsAccountProductTemplate?.balanceFrequency) {
<div class="preview-field">
<span class="preview-label">Balance Frequency</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.balanceFrequency }}</span>
</div>
}
@if (savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing !== undefined) {
<div class="preview-field">
<span class="preview-label">Account Closed Zero Balance In Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing"
>
{{ savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
}
@if (savingsAccount?.minRequiredBalance) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Balance' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.minRequiredBalance }}</span>
</div>
}
</div>
</div>
</div>
@if (hasDigitalSection) {
<div class="preview-section">
<div class="preview-section__header">
@@ -424,58 +232,6 @@
</div>
}
</div>
@if (digitalPreview?.accountFacilityLimits?.length) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Limit Type</th>
<th>General Limit</th>
<th>Minor Account Limit</th>
<th>Small Saving Limit</th>
</tr>
</thead>
<tbody>
@for (row of digitalPreview.accountFacilityLimits; track row.limitType) {
<tr>
<td>{{ row.limitType }}</td>
<td>{{ row.generalLimit || 'Unassigned' }}</td>
<td>{{ row.minorAccountLimit || 'Unassigned' }}</td>
<td>{{ row.smallSavingLimit || 'Unassigned' }}</td>
</tr>
}
</tbody>
</table>
}
@if (digitalPreview?.depositLimitKyc?.general) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Account Type</th>
<th>KYC Type</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>{{ digitalPreview.depositLimitKyc.general.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.general.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Minor</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Small Saving</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.limit || 'Unassigned' }}</td>
</tr>
</tbody>
</table>
}
</div>
</div>
}
@@ -501,65 +257,6 @@
</div>
</div>
}
@if (savingsAccount?.charges?.length) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.inputs.Charges' | translate }}</span>
</div>
<div class="preview-body">
<table class="mat-elevation-z1 preview-table" mat-table [dataSource]="savingsAccount.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name + ', ' + charge.currency.displaySymbol }}</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (
charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee'
) {
<span>{{ (charge.dueDate | dateFormat) || 'Unassigned' }}</span>
}
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span>{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}</span>
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
) {
<span>N/A</span>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.feeInterval || 'Not Provided' }}</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType.value }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
</div>
}
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
@@ -144,13 +144,7 @@
<mat-form-field appearance="outline" class="sat-field" (click)="lastInterestDatePicker.open()">
<mat-label>Last Interest Date</mat-label>
<input
matInput
[matDatepicker]="lastInterestDatePicker"
formControlName="lastInterestDate"
[min]="minDate"
[max]="maxDate"
/>
<input matInput [matDatepicker]="lastInterestDatePicker" formControlName="lastInterestDate" />
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
@@ -0,0 +1,308 @@
<!--
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/.
-->
@if (shouldShow) {
<div class="dap" [class]="'dap--' + risk">
<!-- ══ HEADER ════════════════════════════════════════════════════ -->
<div class="dap__header">
<div class="dap__header-left">
<div class="dap__icon-wrap">
<i [class]="isProductMode ? 'fas fa-shield-alt' : 'fas fa-exclamation-triangle'"></i>
</div>
<div>
<div class="dap__title">
{{ isProductMode ? 'Dormancy Monitoring Configuration' : 'Dormancy Risk Monitor' }}
</div>
<div class="dap__subtitle">
@if (isProductMode) {
Product-level dormancy thresholds applied to this account
} @else {
Real-time dormancy tracking · Based on last customer transaction
}
</div>
</div>
</div>
<!-- Risk severity badge -->
<div class="dap__badges">
<span
class="dap__risk-badge"
[class]="'dap__risk-badge--' + risk"
title="Dormancy risk level based on transaction inactivity"
>
<span class="dap__risk-dot"></span>
@if (isProductMode) {
Monitoring: {{ riskSeverity }}
} @else {
Dormancy Risk: {{ riskSeverity }}
}
</span>
@if (!isProductMode && isMonitoringActive) {
<span class="dap__monitor-badge"> <i class="fas fa-satellite-dish"></i> Monitoring Active </span>
}
</div>
</div>
<!-- ══ METRIC CARDS ════════════════════════════════════════════ -->
<div class="dap__cards" [class.dap__cards--product]="isProductMode">
@if (isProductMode) {
<!-- ── PRODUCT MODE CARDS ─────────────────────────────────── -->
<!-- Account Status -->
<div class="dap__card">
<span class="dap__card-label">Account Status</span>
<span class="dap__pill dap__pill--green"> <i class="fas fa-check-circle"></i> New Account </span>
<span class="dap__card-hint">No transactions yet</span>
</div>
<!-- Monitoring Status -->
<div class="dap__card">
<span class="dap__card-label">
Dormancy Monitoring
<i
class="fas fa-info-circle dap__help"
title="When enabled, the system tracks transaction activity and alerts on inactivity"
></i>
</span>
<span class="dap__pill" [class]="isMonitoringActive ? 'dap__pill--green' : 'dap__pill--gray'">
<i [class]="isMonitoringActive ? 'fas fa-satellite-dish' : 'fas fa-ban'"></i>
{{ isMonitoringActive ? 'Enabled' : 'Disabled' }}
</span>
<span class="dap__card-hint">{{
isMonitoringActive ? 'Transaction activity tracked' : 'Not configured'
}}</span>
</div>
<!-- Alert Notification -->
<div class="dap__card">
<span class="dap__card-label">
Alert Notifications
<i
class="fas fa-info-circle dap__help"
title="Sends alerts to branch officers when account approaches dormancy threshold"
></i>
</span>
<span class="dap__pill" [class]="isAlertEnabled ? 'dap__pill--green' : 'dap__pill--gray'">
<i [class]="isAlertEnabled ? 'fas fa-bell' : 'fas fa-bell-slash'"></i>
{{ isAlertEnabled ? 'Enabled' : 'Disabled' }}
</span>
<span class="dap__card-hint">{{ isAlertEnabled ? 'Branch will be notified' : 'No alerts configured' }}</span>
</div>
<!-- Inactive Threshold -->
<div class="dap__card">
<span class="dap__card-label">
Inactive Threshold
<i
class="fas fa-info-circle dap__help"
title="Account is flagged inactive after this many days of no customer-initiated transaction"
></i>
</span>
@if (daysToInactive !== null) {
<span class="dap__number"> {{ daysToInactive }}<small>days</small> </span>
<span class="dap__card-hint">No transaction for {{ daysToInactive }} days</span>
} @else {
<span class="dap__na">Not configured</span>
}
</div>
<!-- Dormancy Threshold -->
<div class="dap__card">
<span class="dap__card-label">
Dormancy Threshold
<i
class="fas fa-info-circle dap__help"
title="Account is classified dormant after this many additional days following inactive status"
></i>
</span>
@if (daysToDormancy !== null) {
<span class="dap__number">{{ daysToDormancy }}<small>days</small></span>
<span class="dap__card-hint">{{ daysToDormancy }} days after inactive</span>
} @else {
<span class="dap__na">Not configured</span>
}
</div>
<!-- Escheat Threshold -->
<div class="dap__card">
<span class="dap__card-label">
Escheat Threshold
<i
class="fas fa-info-circle dap__help"
title="Unclaimed funds transferred to government/regulator after this many days of dormancy"
></i>
</span>
@if (daysToEscheat !== null) {
<span class="dap__number">{{ daysToEscheat }}<small>days</small></span>
<span class="dap__card-hint">{{ daysToEscheat }} days after dormant</span>
} @else {
<span class="dap__na">Not configured</span>
}
</div>
} @else {
<!-- ── ACCOUNT VIEW MODE CARDS ────────────────────────────── -->
<!-- Account Status -->
<div class="dap__card">
<span class="dap__card-label">Account Status</span>
<span class="dap__pill" [class]="'dap__pill--' + risk">
<i class="fas fa-circle"></i> {{ statusLabel }}
</span>
<span class="dap__card-hint">
@if (statusId === 0) {
Currently active
}
@if (statusId === 100) {
No recent transactions
}
@if (statusId === 200) {
Classified as dormant
}
@if (statusId === 300) {
Pending regulatory transfer
}
</span>
</div>
<!-- Last Transaction -->
<div class="dap__card">
<span class="dap__card-label">Last Transaction</span>
@if (lastTxnDate) {
<span class="dap__date"><i class="fas fa-calendar-check"></i> {{ lastTxnDate }}</span>
<span class="dap__card-hint">Most recent customer activity</span>
} @else if (accountOpenedDate) {
<span class="dap__date dap__date--muted"><i class="fas fa-calendar-plus"></i> {{ accountOpenedDate }}</span>
<span class="dap__card-hint">Account opened · No transactions yet</span>
} @else {
<span class="dap__na">No transactions yet</span>
}
</div>
<!-- Days to Inactive -->
<div class="dap__card">
<span class="dap__card-label">
Days to Inactive
<i
class="fas fa-info-circle dap__help"
title="Account will be flagged inactive after no customer-initiated transaction for this duration"
></i>
</span>
@if (statusId >= 100) {
<span class="dap__reached"><i class="fas fa-flag"></i> Threshold Reached</span>
<span class="dap__card-hint">Account is inactive</span>
} @else if (daysToInactive !== null) {
<span class="dap__number" [class.dap__number--warn]="daysToInactive <= 30">
{{ daysToInactive }}<small>days</small>
</span>
<span class="dap__card-hint">{{ inactiveSubtext() }}</span>
} @else {
<span class="dap__na">—</span>
}
</div>
<!-- Days to Dormant -->
<div class="dap__card">
<span class="dap__card-label">
Days to Dormant
<i
class="fas fa-info-circle dap__help"
title="Dormant classification per RBI guidelines after prolonged inactivity"
></i>
</span>
@if (statusId >= 200) {
<span class="dap__reached"><i class="fas fa-flag"></i> Threshold Reached</span>
<span class="dap__card-hint">Account is dormant</span>
} @else if (daysToDormancy !== null) {
<span class="dap__number" [class.dap__number--warn]="daysToDormancy <= 30">
{{ daysToDormancy }}<small>days</small>
</span>
<span class="dap__card-hint">{{ dormancySubtext() }}</span>
} @else {
<span class="dap__na">—</span>
}
</div>
<!-- Days to Escheat -->
<div class="dap__card">
<span class="dap__card-label">
Days to Escheat
<i
class="fas fa-info-circle dap__help"
title="Unclaimed funds transferred to government if account remains dormant beyond this period"
></i>
</span>
@if (statusId >= 300) {
<span class="dap__reached"><i class="fas fa-flag"></i> Threshold Reached</span>
<span class="dap__card-hint">Pending regulatory transfer</span>
} @else if (daysToEscheat !== null) {
<span class="dap__number" [class.dap__number--warn]="daysToEscheat <= 30">
{{ daysToEscheat }}<small>days</small>
</span>
<span class="dap__card-hint">{{ escheatSubtext() }}</span>
} @else {
<span class="dap__na">—</span>
}
</div>
}
</div>
<!-- ══ DORMANCY PROGRESSION TIMELINE ══════════════════════════ -->
<div class="dap__timeline">
<div class="dap__tl-header">
<span class="dap__tl-label">Dormancy Progression</span>
@if (!isProductMode) {
<span class="dap__tl-note">Timeline based on last customer transaction</span>
}
</div>
<div class="dap__tl-track">
<!-- Active node (always reached) -->
<div class="dap__tl-node" [class.dap__tl-node--current]="statusId === 0" [class.dap__tl-node--reached]="true">
<div class="dap__tl-circle dap__tl-circle--done">
<i class="fas fa-check"></i>
</div>
<span class="dap__tl-name">Active</span>
<span class="dap__tl-sub">Normal</span>
</div>
@for (step of timelineSteps; track step.label) {
<div class="dap__tl-line" [class.dap__tl-line--done]="step.reached"></div>
<div
class="dap__tl-node"
[class.dap__tl-node--current]="step.current"
[class.dap__tl-node--reached]="step.reached"
>
<div
class="dap__tl-circle"
[class.dap__tl-circle--done]="step.reached && !step.current"
[class.dap__tl-circle--current]="step.current"
[class.dap__tl-circle--pending]="!step.reached"
>
@if (step.reached && !step.current) {
<i class="fas fa-check"></i>
} @else if (step.current) {
<i class="fas fa-circle"></i>
} @else {
<i class="far fa-circle"></i>
}
</div>
<span class="dap__tl-name" [title]="step.tooltip">
{{ step.label }}
<i class="fas fa-info-circle dap__tl-info"></i>
</span>
@if (!step.reached && step.days !== null) {
<span class="dap__tl-days">{{ step.days }}d</span>
} @else if (step.reached) {
<span class="dap__tl-reached-tag">Reached</span>
}
</div>
}
</div>
</div>
</div>
}
@@ -0,0 +1,684 @@
/**
* 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/.
*/
@use 'sass:color';
// ── Design Tokens ──────────────────────────────────────────────────────────
// Risk colours
$c-safe: #16a34a;
$bg-safe: #f0fdf4;
$bd-safe: #bbf7d0;
$c-low: #0284c7;
$bg-low: #f0f9ff;
$bd-low: #bae6fd;
$c-medium: #d97706;
$bg-medium: #fffbeb;
$bd-medium: #fde68a;
$c-high: #dc2626;
$bg-high: #fff5f5;
$bd-high: #fecaca;
$c-critical: #b91c1c;
$bg-critical: #fef2f2;
$bd-critical: #fca5a5;
$c-escheat: #7c3aed;
$bg-escheat: #f5f3ff;
$bd-escheat: #ddd6fe;
// Neutral
$text: #1e293b;
$muted: #64748b;
$subtle: #94a3b8;
$border: #e2e8f0;
$surface: #f8fafc;
// ── Host ───────────────────────────────────────────────────────────────────
:host {
display: block;
margin: 14px 0 18px;
}
// ── Root wrapper ───────────────────────────────────────────────────────────
.dap {
border-radius: 14px;
border: 1.5px solid $border;
background: #fff;
overflow: hidden;
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 3px 12px rgb(0 0 0 / 10%);
}
// Risk-tinted border
&--safe {
border-color: $bd-safe;
}
&--low {
border-color: $bd-low;
}
&--medium {
border-color: $bd-medium;
}
&--high {
border-color: $bd-high;
}
&--critical {
border-color: $bd-critical;
}
&--escheat {
border-color: $bd-escheat;
}
// ── HEADER ───────────────────────────────────────────────────────
&__header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 14px 18px;
background: $surface;
border-bottom: 1px solid $border;
}
&__header-left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon-wrap {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
border: 1px solid $border;
i {
font-size: 17px;
}
.dap--safe & {
background: $bg-safe;
border-color: $bd-safe;
i {
color: $c-safe;
}
}
.dap--low & {
background: $bg-low;
border-color: $bd-low;
i {
color: $c-low;
}
}
.dap--medium & {
background: $bg-medium;
border-color: $bd-medium;
i {
color: $c-medium;
}
}
.dap--high & {
background: $bg-high;
border-color: $bd-high;
i {
color: $c-high;
}
}
.dap--critical & {
background: $bg-critical;
border-color: $bd-critical;
i {
color: $c-critical;
}
}
.dap--escheat & {
background: $bg-escheat;
border-color: $bd-escheat;
i {
color: $c-escheat;
}
}
}
&__title {
font-size: 13.5px;
font-weight: 700;
color: $text;
letter-spacing: -0.01em;
line-height: 1.3;
}
&__subtitle {
font-size: 11.5px;
color: $muted;
margin-top: 2px;
line-height: 1.4;
}
// ── Badge group ──────────────────────────────────────────────────
&__badges {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
&__risk-badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 13px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.01em;
cursor: default;
.dap--safe & {
background: $bg-safe;
color: $c-safe;
border: 1.5px solid $bd-safe;
}
.dap--low & {
background: $bg-low;
color: $c-low;
border: 1.5px solid $bd-low;
}
.dap--medium & {
background: $bg-medium;
color: $c-medium;
border: 1.5px solid $bd-medium;
}
.dap--high & {
background: $bg-high;
color: $c-high;
border: 1.5px solid $bd-high;
}
.dap--critical & {
background: $bg-critical;
color: $c-critical;
border: 1.5px solid $bd-critical;
}
.dap--escheat & {
background: $bg-escheat;
color: $c-escheat;
border: 1.5px solid $bd-escheat;
}
}
&__risk-dot {
width: 7px;
height: 7px;
border-radius: 50%;
display: inline-block;
animation: pulse-dot 2s ease-in-out infinite;
.dap--safe & {
background: $c-safe;
}
.dap--low & {
background: $c-low;
}
.dap--medium & {
background: $c-medium;
}
.dap--high & {
background: $c-high;
}
.dap--critical & {
background: $c-critical;
}
.dap--escheat & {
background: $c-escheat;
}
}
&__monitor-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 11px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: $bg-low;
color: $c-low;
border: 1.5px solid $bd-low;
i {
font-size: 10px;
}
}
// ── Metric Cards ─────────────────────────────────────────────────
&__cards {
display: grid;
grid-template-columns: repeat(5, 1fr);
border-bottom: 1px solid $border;
&--product {
grid-template-columns: repeat(6, 1fr);
}
@media (width <= 1024px) {
grid-template-columns: repeat(3, 1fr) !important;
.dap__card:nth-child(3n) {
border-right: none;
}
}
@media (width <= 600px) {
grid-template-columns: repeat(2, 1fr) !important;
.dap__card:nth-child(2n) {
border-right: none;
}
}
}
&__card {
padding: 14px 16px;
border-right: 1px solid $border;
display: flex;
flex-direction: column;
gap: 5px;
transition: background 0.15s;
&:last-child {
border-right: none;
}
&:hover {
background: $surface;
}
}
&__card-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $muted;
display: flex;
align-items: center;
gap: 4px;
}
&__card-hint {
font-size: 10.5px;
color: $subtle;
margin-top: 2px;
line-height: 1.3;
}
&__help {
font-size: 9px;
color: $subtle;
cursor: help;
&:hover {
color: $c-low;
}
}
// Status pill
&__pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 11px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
width: fit-content;
i {
font-size: 11px;
}
&--green {
background: $bg-safe;
color: $c-safe;
border: 1px solid $bd-safe;
}
&--gray {
background: #f1f5f9;
color: $muted;
border: 1px solid #e2e8f0;
}
&--safe {
background: $bg-safe;
color: $c-safe;
border: 1px solid $bd-safe;
}
&--low {
background: $bg-low;
color: $c-low;
border: 1px solid $bd-low;
}
&--medium {
background: $bg-medium;
color: $c-medium;
border: 1px solid $bd-medium;
}
&--high {
background: $bg-high;
color: $c-high;
border: 1px solid $bd-high;
}
&--critical {
background: $bg-critical;
color: $c-critical;
border: 1px solid $bd-critical;
}
&--escheat {
background: $bg-escheat;
color: $c-escheat;
border: 1px solid $bd-escheat;
}
}
// Number display
&__number {
font-size: 22px;
font-weight: 800;
color: $text;
display: flex;
align-items: baseline;
gap: 4px;
font-variant-numeric: tabular-nums;
small {
font-size: 11px;
font-weight: 500;
color: $muted;
}
&--warn {
color: $c-high;
small {
color: color.adjust($c-high, $lightness: 10%);
}
}
}
&__date {
font-size: 12.5px;
font-weight: 600;
color: $text;
display: flex;
align-items: center;
gap: 6px;
i {
color: $muted;
font-size: 12px;
}
&--muted {
color: $muted;
}
}
&__reached {
font-size: 12px;
font-weight: 600;
color: $c-high;
display: flex;
align-items: center;
gap: 6px;
i {
font-size: 11px;
}
}
&__na {
font-size: 12px;
color: $subtle;
font-style: italic;
}
// ── Timeline ─────────────────────────────────────────────────────
&__timeline {
padding: 16px 18px 18px;
background: $surface;
border-top: 1px solid $border;
}
&__tl-header {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 16px;
}
&__tl-label {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
color: $muted;
}
&__tl-note {
font-size: 10px;
color: $subtle;
font-style: italic;
}
&__tl-track {
display: flex;
align-items: flex-start;
overflow-x: auto;
padding-bottom: 4px;
}
&__tl-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
flex: 0 0 auto;
min-width: 68px;
}
&__tl-circle {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid $border;
background: #fff;
transition: all 0.2s;
i {
font-size: 12px;
color: #cbd5e1;
}
&--done {
background: $c-safe;
border-color: $c-safe;
box-shadow: 0 0 0 3px rgba($c-safe, 0.15);
i {
color: #fff;
}
}
&--current {
background: #fff;
box-shadow: 0 0 0 3px rgb(0 0 0 / 8%);
.dap--safe &,
.dap--low & {
border-color: $c-safe;
box-shadow: 0 0 0 3px rgba($c-safe, 0.15);
i {
color: $c-safe;
}
}
.dap--medium & {
border-color: $c-medium;
box-shadow: 0 0 0 3px rgba($c-medium, 0.15);
i {
color: $c-medium;
}
}
.dap--high &,
.dap--critical & {
border-color: $c-high;
box-shadow: 0 0 0 3px rgba($c-high, 0.15);
i {
color: $c-high;
}
}
.dap--escheat & {
border-color: $c-escheat;
box-shadow: 0 0 0 3px rgba($c-escheat, 0.15);
i {
color: $c-escheat;
}
}
}
&--pending {
background: $surface;
i {
color: #cbd5e1;
}
}
}
&__tl-name {
font-size: 10px;
font-weight: 700;
color: $muted;
text-transform: uppercase;
letter-spacing: 0.04em;
text-align: center;
white-space: nowrap;
cursor: help;
display: flex;
align-items: center;
gap: 3px;
}
&__tl-info {
font-size: 8px;
color: $subtle;
text-transform: none;
}
&__tl-sub {
font-size: 9px;
color: $subtle;
text-align: center;
}
&__tl-days {
font-size: 10px;
color: $muted;
background: #e9eef5;
border-radius: 999px;
padding: 2px 7px;
font-weight: 600;
white-space: nowrap;
}
&__tl-reached-tag {
font-size: 9px;
color: $c-high;
background: $bg-high;
border-radius: 999px;
padding: 2px 7px;
font-weight: 700;
border: 1px solid $bd-high;
white-space: nowrap;
}
&__tl-line {
flex: 1;
height: 2px;
background: $border;
margin: 14px 2px 0;
min-width: 20px;
border-radius: 2px;
transition: background 0.3s;
&--done {
background: $c-safe;
}
}
}
// ── Pulse animation for risk dot ───────────────────────────────────────────
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
@@ -0,0 +1,195 @@
/**
* 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, Input, OnInit } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export type DormancyRisk = 'safe' | 'low' | 'medium' | 'high' | 'critical' | 'escheat';
@Component({
selector: 'mifosx-dormancy-alert-panel',
templateUrl: './dormancy-alert-panel.component.html',
styleUrls: ['./dormancy-alert-panel.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class DormancyAlertPanelComponent implements OnInit {
/** Pass full account data — for ACCOUNT VIEW (general tab) */
@Input() accountData: any;
/** Pass product template — for ACCOUNT CREATION (currency step) */
@Input() productTemplate: any;
risk: DormancyRisk = 'safe';
riskLabel = '';
riskSeverity = ''; // e.g. "Low", "Medium", "High"
statusLabel = '';
statusId = 0;
lastTxnDate: string | null = null;
accountOpenedDate: string | null = null;
daysToInactive: number | null = null;
daysToDormancy: number | null = null;
daysToEscheat: number | null = null;
isMonitoringActive = false; // isDormancyTrackingActive
isAlertEnabled = false; // dormancyAlert
shouldShow = false;
/** true = product template mode (creation stepper), false = account view mode */
get isProductMode(): boolean {
return !!this.productTemplate && !this.accountData;
}
ngOnInit(): void {
if (this.productTemplate && !this.accountData) {
this.initFromProductTemplate();
} else if (this.accountData) {
this.initFromAccountData();
}
}
// ── Product Template Mode (Currency Step) ─────────────────────
private initFromProductTemplate(): void {
const pt = this.productTemplate;
// Savings: fields come with "FromProduct" suffix
// RD/Current: fields come directly (isDormancyTrackingActive, daysToInactive, etc.)
this.isMonitoringActive = !!(pt.isDormancyTrackingActiveFromProduct ?? pt.isDormancyTrackingActive ?? false);
this.isAlertEnabled = !!pt.dormancyAlert;
this.daysToInactive = pt.daysToInactiveFromProduct ?? pt.daysToInactive ?? null;
this.daysToDormancy = pt.daysToDormancyFromProduct ?? pt.daysToDormancy ?? null;
this.daysToEscheat = pt.daysToEscheatFromProduct ?? pt.daysToEscheat ?? null;
this.statusId = 0;
this.statusLabel = 'New Account';
this.computeRiskFromProduct();
this.shouldShow = this.isMonitoringActive || this.isAlertEnabled;
}
private computeRiskFromProduct(): void {
// Product mode — always show safe/neutral, no risk colouring
this.risk = 'safe';
this.riskLabel = 'Configured';
this.riskSeverity = this.isMonitoringActive ? 'Active' : 'Inactive';
}
// ── Account Data Mode (General Tab) ───────────────────────────
private initFromAccountData(): void {
const a = this.accountData;
this.statusId = a.subStatus?.id ?? 0;
this.statusLabel = a.subStatus?.value ?? 'Active';
this.isAlertEnabled = !!a.dormancyAlert;
this.isMonitoringActive = !!a.isDormancyTrackingActive;
this.daysToInactive = a.daysToInactive ?? null;
this.daysToDormancy = a.daysToDormancy ?? null;
this.daysToEscheat = a.daysToEscheat ?? null;
// Last active transaction date
const raw = a.lastActiveTransactionDate;
if (raw) {
const d = Array.isArray(raw) && raw.length >= 3 ? new Date(raw[0], raw[1] - 1, raw[2]) : new Date(raw);
this.lastTxnDate = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
// Account opened / activated date
const tl = a.timeline;
if (tl?.activatedOnDate) {
const ad =
Array.isArray(tl.activatedOnDate) && tl.activatedOnDate.length >= 3
? new Date(tl.activatedOnDate[0], tl.activatedOnDate[1] - 1, tl.activatedOnDate[2])
: new Date(tl.activatedOnDate);
this.accountOpenedDate = ad.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
this.computeRiskFromAccount();
this.shouldShow =
this.isAlertEnabled ||
this.isMonitoringActive ||
this.statusId > 0 ||
(this.daysToInactive !== null && this.daysToInactive <= 90) ||
!!this.lastTxnDate;
}
private computeRiskFromAccount(): void {
if (this.statusId === 300) {
this.risk = 'escheat';
this.riskLabel = 'Escheat';
this.riskSeverity = 'Critical';
} else if (this.statusId === 200) {
this.risk = 'critical';
this.riskLabel = 'Dormant';
this.riskSeverity = 'Critical';
} else if (this.statusId === 100) {
this.risk = 'high';
this.riskLabel = 'Inactive';
this.riskSeverity = 'High';
} else if (this.daysToInactive !== null && this.daysToInactive <= 30) {
this.risk = 'high';
this.riskLabel = 'High';
this.riskSeverity = 'High';
} else if (this.daysToInactive !== null && this.daysToInactive <= 60) {
this.risk = 'medium';
this.riskLabel = 'Medium';
this.riskSeverity = 'Medium';
} else if (this.daysToInactive !== null && this.daysToInactive <= 90) {
this.risk = 'low';
this.riskLabel = 'Low';
this.riskSeverity = 'Low';
} else {
this.risk = 'safe';
this.riskLabel = 'Active';
this.riskSeverity = 'Safe';
}
}
get timelineSteps() {
return [
{
label: 'Inactive',
tooltip: 'Account flagged as inactive after no customer-initiated transaction',
days: this.daysToInactive,
reached: this.statusId >= 100,
current: this.statusId === 100
},
{
label: 'Dormant',
tooltip: 'Account classified as dormant per RBI/regulatory guidelines',
days: this.daysToDormancy,
reached: this.statusId >= 200,
current: this.statusId === 200
},
{
label: 'Escheat',
tooltip: 'Unclaimed funds transferred to government/regulator as per law',
days: this.daysToEscheat,
reached: this.statusId >= 300,
current: this.statusId === 300
}
];
}
/** e.g. "Inactive after 30 days of no transaction" */
inactiveSubtext(): string {
return this.daysToInactive !== null ? `No transaction for ${this.daysToInactive} days` : 'Threshold not configured';
}
dormancySubtext(): string {
const total = (this.daysToInactive ?? 0) + (this.daysToDormancy ?? 0);
return this.daysToDormancy !== null ? `${this.daysToDormancy} days after inactive` : 'Threshold not configured';
}
escheatSubtext(): string {
return this.daysToEscheat !== null ? `${this.daysToEscheat} days after dormant` : 'Threshold not configured';
}
}
@@ -0,0 +1,130 @@
<!--
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="intro-section">
<div class="intro-section__header">
<i class="fas fa-user-check"></i>
<span>Introducer</span>
<span class="intro-badge intro-badge--required">Required</span>
</div>
<div class="intro-body">
@if (!selectedClient) {
<!-- Search Box -->
<div class="intro-search-wrap">
<mat-form-field appearance="outline" class="intro-search-field">
<mat-label>Search by Name or Client ID</mat-label>
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" autocomplete="off" />
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
@if (searching) {
<div class="intro-results">
<div class="intro-results__loading">
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
<span>Searching...</span>
</div>
</div>
}
@if (!searching && searchResults.length > 0) {
<div class="intro-results-inline">
@for (r of searchResults; track r.id) {
<div class="intro-result-item" (click)="selectClient(r)">
<mat-icon class="intro-result-item__icon">person</mat-icon>
<div class="intro-result-item__info">
<span class="intro-result-item__name">{{ r.displayName }}</span>
<span class="intro-result-item__meta">
ID: {{ r.id }}
@if (r.officeName) {
· {{ r.officeName }}
}
@if (r.mobileNo) {
· {{ r.mobileNo }}
}
</span>
</div>
</div>
}
</div>
}
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
<div class="intro-no-results">No clients found</div>
}
</div>
}
@if (selectedClient) {
<!-- Selected Client Card -->
<div class="intro-card">
<div class="intro-card__avatar">
@if (linkedProfileImage) {
<img [src]="linkedProfileImage" alt="Profile" class="intro-card__photo" />
} @else {
<mat-icon class="intro-card__avatar-icon">person</mat-icon>
}
</div>
<div class="intro-card__info">
<span class="intro-card__name">{{ selectedClient.displayName }}</span>
<span class="intro-card__meta">
Client ID: {{ selectedClient.id }}
@if (selectedClient.accountNumber) {
· A/C: {{ selectedClient.accountNumber }}
}
</span>
</div>
<button mat-icon-button type="button" (click)="removeClient()" class="intro-card__remove">
<mat-icon>close</mat-icon>
</button>
</div>
@if (fetchingLinkedData) {
<div class="intro-fetching">
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
<span>Fetching client details...</span>
</div>
}
@if (!fetchingLinkedData) {
<div class="intro-details-grid">
<div class="intro-detail">
<span class="intro-detail__label">FULL NAME</span>
<span class="intro-detail__value">{{ selectedClient.displayName }}</span>
</div>
<div class="intro-detail">
<span class="intro-detail__label">DATE OF BIRTH</span>
<span class="intro-detail__value">{{ clientDob || '—' }}</span>
</div>
@if (clientAadhaar) {
<div class="intro-detail">
<span class="intro-detail__label">AADHAAR</span>
<span class="intro-detail__value">
{{ clientAadhaar }}
@if (linkedKycStatus?.aadhaarVerified) {
<span class="intro-verified"><mat-icon>verified</mat-icon> Verified</span>
}
</span>
</div>
}
@if (clientPan) {
<div class="intro-detail">
<span class="intro-detail__label">PAN</span>
<span class="intro-detail__value">
{{ clientPan }}
@if (linkedKycStatus?.panVerified) {
<span class="intro-verified"><mat-icon>verified</mat-icon> Verified</span>
}
</span>
</div>
}
</div>
}
}
</div>
</div>
@@ -0,0 +1,316 @@
/**
* 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/.
*/
$blue: #2563eb;
$blue-light: #eff6ff;
$green: #16a34a;
$green-light: #dcfce7;
$border: #e2e8f0;
$text: #1e293b;
$muted: #64748b;
$gray-bg: #f8fafc;
:host {
overflow: visible;
}
.intro-section {
border: 1px solid $border;
border-radius: 12px;
overflow: visible;
background: #fff;
margin-top: 16px;
position: relative;
z-index: 10;
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
background: $gray-bg;
border-bottom: 1px solid $border;
border-radius: 12px 12px 0 0;
i {
color: $blue;
font-size: 14px;
}
span:nth-child(2) {
font-size: 14px;
font-weight: 600;
color: $text;
flex: 1;
}
}
}
.intro-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
&--required {
background: #fef3c7;
color: #92400e;
border: 1px solid #fde68a;
}
}
.intro-body {
padding: 16px 20px;
overflow: visible;
position: relative;
z-index: 1000;
}
// Search
.intro-search-wrap {
position: relative;
z-index: 1000;
overflow: visible !important;
}
.intro-search-field {
width: 100%;
}
.intro-results-inline {
margin-top: 4px;
border: 1px solid $border;
border-radius: 8px;
background: #fff;
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
overflow: hidden;
max-height: 260px;
overflow-y: auto;
}
.intro-results {
position: absolute !important;
z-index: 99999 !important;
top: calc(100% + 4px);
left: 0;
right: 0;
background: #fff;
border: 1px solid $border;
border-radius: 8px;
box-shadow: 0 4px 16px rgb(0 0 0 / 10%);
max-height: 260px;
overflow: hidden auto;
&__loading {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
font-size: 13px;
color: $muted;
}
}
.intro-result-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid $border;
min-width: 0;
&:last-child {
border-bottom: none;
}
&:hover {
background: $blue-light;
}
&__icon {
color: $muted;
font-size: 20px;
width: 20px;
height: 20px;
flex: 0 0 auto;
}
&__info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
&__name {
font-size: 13px;
font-weight: 600;
color: $text;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
}
&__meta {
font-size: 11px;
color: $muted;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
}
}
.intro-no-results {
padding: 10px 0;
font-size: 13px;
color: $muted;
text-align: center;
}
// Selected client card
.intro-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid $border;
border-radius: 10px;
background: $gray-bg;
margin-bottom: 14px;
min-width: 0;
&__avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: $blue-light;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
overflow: hidden;
}
&__photo {
width: 100%;
height: 100%;
object-fit: cover;
}
&__avatar-icon {
color: $blue;
font-size: 24px;
width: 24px;
height: 24px;
}
&__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
&__name {
font-size: 14px;
font-weight: 600;
color: $text;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
}
&__meta {
font-size: 12px;
color: $muted;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
}
&__remove {
flex: 0 0 auto;
color: $muted;
}
}
.intro-fetching {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: $muted;
padding: 6px 0;
}
// Details grid
.intro-details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px 24px;
padding: 14px 16px;
border: 1px solid $border;
border-radius: 10px;
background: #fff;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.intro-detail {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
&__label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
color: $muted;
text-transform: uppercase;
}
&__value {
font-size: 13px;
font-weight: 600;
color: $text;
display: flex;
align-items: center;
gap: 6px;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
flex-wrap: wrap;
}
}
.intro-verified {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-weight: 600;
color: $green;
background: $green-light;
border-radius: 999px;
padding: 2px 8px;
mat-icon {
font-size: 12px;
width: 12px;
height: 12px;
}
}
@@ -0,0 +1,138 @@
/**
* 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, Output, EventEmitter, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { ClientsService } from 'app/clients/clients.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'mifosx-introducer-search',
templateUrl: './introducer-search.component.html',
styleUrls: ['./introducer-search.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
MatProgressSpinner,
MatIconButton,
MatIcon
]
})
export class IntroducerSearchComponent {
private clientsService = inject(ClientsService);
private sanitizer = inject(DomSanitizer);
@Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string } | null>();
entryMode: 'linked' = 'linked';
searchText = '';
searching = false;
searchResults: any[] = [];
selectedClient: any = null;
fetchingLinkedData = false;
linkedProfileImage: any = null;
linkedKycStatus: any = null;
// Fetched client details
clientDob = '';
clientAadhaar = '';
clientPan = '';
private searchTimer: any;
onSearch(text: string): void {
this.searchText = text;
clearTimeout(this.searchTimer);
if (!text || text.length < 2) {
this.searchResults = [];
return;
}
this.searching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8).subscribe({
next: (data: any) => {
this.searchResults = data?.content || [];
this.searching = false;
},
error: () => {
this.searchResults = [];
this.searching = false;
}
});
}, 400);
}
selectClient(client: any): void {
this.selectedClient = client;
this.searchResults = [];
this.searchText = '';
this.clientDob = '';
this.clientAadhaar = '';
this.clientPan = '';
this.linkedProfileImage = null;
this.linkedKycStatus = null;
this.fetchLinkedClientData(client.id);
this.introducerChange.emit({ clientId: client.id, displayName: client.displayName });
}
removeClient(): void {
this.selectedClient = null;
this.linkedProfileImage = null;
this.linkedKycStatus = null;
this.clientDob = '';
this.clientAadhaar = '';
this.clientPan = '';
this.introducerChange.emit(null);
}
private fetchLinkedClientData(clientId: string): void {
this.fetchingLinkedData = true;
this.clientsService.getClientData(clientId).subscribe({
next: (data: any) => {
if (data.dateOfBirth) {
const d = Array.isArray(data.dateOfBirth)
? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2])
: new Date(data.dateOfBirth);
this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
this.fetchingLinkedData = false;
},
error: () => {
this.fetchingLinkedData = false;
}
});
this.clientsService.getClientKyc(clientId).subscribe({
next: (kyc: any) => {
if (kyc) {
this.clientAadhaar = kyc.aadhaarNumberMasked || '';
this.clientPan = kyc.panNumber || '';
this.linkedKycStatus = {
panVerified: kyc.panVerified || false,
aadhaarVerified: kyc.aadhaarVerified || false
};
}
},
error: () => {}
});
this.clientsService.getClientProfileImage(clientId).subscribe({
next: (imageData: any) => {
if (imageData) {
this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData);
}
},
error: () => {}
});
}
}
@@ -0,0 +1,282 @@
<!--
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="nom-section">
<!-- Header -->
<div class="nom-header">
<div class="nom-header__left">
<i class="fas fa-user-shield"></i>
<span class="nom-header__title">Nomination Facility</span>
</div>
<div class="nom-mode-tabs">
<button type="button" class="nom-tab" [class.nom-tab--active]="mode === 'manual'" (click)="switchMode('manual')">
<i class="fas fa-pencil-alt"></i> Enter Manually
</button>
<button type="button" class="nom-tab" [class.nom-tab--active]="mode === 'linked'" (click)="switchMode('linked')">
<i class="fas fa-search"></i> Link Existing Client
</button>
</div>
</div>
<div class="nom-body">
<!-- ══════════════ MANUAL MODE ══════════════ -->
@if (mode === 'manual') {
<form [formGroup]="manualForm" class="nom-form">
<!-- Basic Info -->
<div class="nom-row">
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Full Name *</mat-label>
<input matInput formControlName="fullName" placeholder="Nominee full name" />
<mat-error>Full name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Relationship *</mat-label>
<mat-select formControlName="relationship">
@for (rel of relationships; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
<mat-error>Relationship is required</mat-error>
</mat-form-field>
</div>
<div class="nom-row">
<mat-form-field appearance="outline" class="nom-field" (click)="dobPicker.open()">
<mat-label>Date of Birth</mat-label>
<input
matInput
[matDatepicker]="dobPicker"
[min]="minDate"
[max]="maxDate"
formControlName="dateOfBirth"
readonly
/>
<mat-datepicker-toggle matSuffix [for]="dobPicker"></mat-datepicker-toggle>
<mat-datepicker #dobPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Nationality</mat-label>
<mat-select formControlName="nationality">
<mat-option value="Indian">Indian</mat-option>
<mat-option value="NRI">NRI</mat-option>
<mat-option value="Foreign National">Foreign National</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- ID Verification -->
<div class="nom-section-label"><i class="fas fa-id-card"></i> ID Verification</div>
<div class="nom-row">
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Aadhaar Number</mat-label>
<input matInput formControlName="aadhaarNumber" maxlength="12" placeholder="12-digit Aadhaar" />
</mat-form-field>
<mat-form-field appearance="outline" class="nom-field">
<mat-label>PAN Number</mat-label>
<input
matInput
formControlName="panNumber"
maxlength="10"
placeholder="ABCDE1234F"
style="text-transform: uppercase"
/>
</mat-form-field>
</div>
<div class="nom-row">
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Mobile Number</mat-label>
<input matInput formControlName="mobileNo" maxlength="10" placeholder="10-digit mobile" />
</mat-form-field>
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Nominee Share (%)</mat-label>
<input matInput type="number" formControlName="nomineeShare" readonly />
</mat-form-field>
</div>
<!-- Guardian (shown if minor / not alive) -->
<div class="nom-section-label">
<i class="fas fa-user-friends"></i> Guardian Details
<span class="nom-hint">(if nominee is a minor)</span>
</div>
<div class="nom-row">
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Guardian Name</mat-label>
<input matInput formControlName="guardianName" />
</mat-form-field>
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Guardian Relationship</mat-label>
<mat-select formControlName="guardianRelationship">
@for (rel of relationships; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<!-- Flags -->
<div class="nom-flags">
<mat-checkbox formControlName="isAlive" color="primary">Is Alive</mat-checkbox>
<mat-checkbox formControlName="isDependent" color="primary">Is Dependent</mat-checkbox>
</div>
<!-- Remarks -->
<mat-form-field appearance="outline" class="nom-field nom-field--full">
<mat-label>Remarks</mat-label>
<textarea matInput formControlName="remarks" rows="2"></textarea>
</mat-form-field>
</form>
}
<!-- ══════════════ LINK EXISTING CLIENT MODE ══════════════ -->
@if (mode === 'linked') {
@if (!selectedClient) {
<!-- Search -->
<div class="nom-search-wrap">
<mat-form-field appearance="outline" class="nom-field--full">
<mat-label>Search by Name or Client ID</mat-label>
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" autocomplete="off" />
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
@if (searching) {
<div class="nom-results">
<div class="nom-results__loading">
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
<span>Searching...</span>
</div>
</div>
}
@if (!searching && searchResults.length > 0) {
<div class="nom-results">
@for (r of searchResults; track r.id) {
<div class="nom-result-item" (click)="selectClient(r)">
<mat-icon class="nom-result-item__icon">person</mat-icon>
<div class="nom-result-item__info">
<span class="nom-result-item__name">{{ r.displayName }}</span>
<span class="nom-result-item__meta">
ID: {{ r.id }}
@if (r.officeName) {
· {{ r.officeName }}
}
@if (r.mobileNo) {
· {{ r.mobileNo }}
}
</span>
</div>
</div>
}
</div>
}
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
<div class="nom-no-results">No clients found</div>
}
</div>
}
@if (selectedClient) {
<!-- Selected Client Card -->
<div class="nom-client-card">
<div class="nom-client-card__avatar">
@if (linkedProfileImage) {
<img [src]="linkedProfileImage" alt="Profile" class="nom-client-card__photo" />
} @else {
<mat-icon class="nom-client-card__avatar-icon">person</mat-icon>
}
</div>
<div class="nom-client-card__info">
<span class="nom-client-card__name">{{ selectedClient.displayName }}</span>
<span class="nom-client-card__meta">
Client ID: {{ selectedClient.id }}
@if (selectedClient.accountNumber) {
· A/C: {{ selectedClient.accountNumber }}
}
</span>
</div>
<button
mat-icon-button
type="button"
(click)="removeLinked()"
class="nom-client-card__remove"
matTooltip="Remove"
>
<mat-icon>close</mat-icon>
</button>
</div>
@if (fetchingLinkedData) {
<div class="nom-fetching">
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
<span>Fetching nominee details...</span>
</div>
}
@if (!fetchingLinkedData) {
<div class="nom-details-grid">
<div class="nom-detail">
<span class="nom-detail__label">Full Name</span>
<span class="nom-detail__value">{{ selectedClient.displayName }}</span>
</div>
<div class="nom-detail">
<span class="nom-detail__label">Date of Birth</span>
<span class="nom-detail__value">{{ clientDob || '—' }}</span>
</div>
@if (clientAadhaar) {
<div class="nom-detail">
<span class="nom-detail__label">Aadhaar</span>
<span class="nom-detail__value">
{{ clientAadhaar }}
@if (linkedKycStatus?.aadhaarVerified) {
<span class="nom-verified"><mat-icon>verified</mat-icon> Verified</span>
}
</span>
</div>
}
@if (clientPan) {
<div class="nom-detail">
<span class="nom-detail__label">PAN</span>
<span class="nom-detail__value">
{{ clientPan }}
@if (linkedKycStatus?.panVerified) {
<span class="nom-verified"><mat-icon>verified</mat-icon> Verified</span>
}
</span>
</div>
}
</div>
<!-- Relationship picker for linked client -->
<div class="nom-linked-extras">
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Relationship *</mat-label>
<mat-select [(ngModel)]="linkedRelationship" (ngModelChange)="onLinkedRelationshipChange($event)">
@for (rel of relationships; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="nom-field">
<mat-label>Nominee Share (%)</mat-label>
<input matInput type="number" [value]="100" readonly />
</mat-form-field>
</div>
}
}
}
</div>
</div>
@@ -0,0 +1,387 @@
/**
* 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/.
*/
@use 'sass:color';
$blue: #2563eb;
$blue-light: #eff6ff;
$green: #16a34a;
$green-light: #dcfce7;
$border: #e2e8f0;
$text: #1e293b;
$muted: #64748b;
$gray-bg: #f8fafc;
$purple: #7c3aed;
$purple-light: #f5f3ff;
:host {
display: block;
}
// ─── Wrapper ───────────────────────────────────────────────────
.nom-section {
border: 1px solid $border;
border-radius: 14px;
background: #fff;
margin-top: 20px;
overflow: visible;
position: relative;
z-index: 10;
}
// ─── Header ────────────────────────────────────────────────────
.nom-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 14px 20px;
background: $gray-bg;
border-bottom: 1px solid $border;
border-radius: 14px 14px 0 0;
&__left {
display: flex;
align-items: center;
gap: 10px;
i {
color: $purple;
font-size: 15px;
}
}
&__title {
font-size: 14px;
font-weight: 700;
color: $text;
}
}
// ─── Mode toggle tabs ──────────────────────────────────────────
.nom-mode-tabs {
display: flex;
gap: 8px;
}
.nom-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: 1.5px solid $border;
background: #fff;
color: $muted;
transition: all 0.18s;
i {
font-size: 11px;
}
&:hover {
border-color: $blue;
color: $blue;
background: $blue-light;
}
&--active {
border-color: $blue;
background: $blue;
color: #fff;
&:hover {
background: color.adjust(#2563eb, $lightness: -5%);
}
}
}
// ─── Body ──────────────────────────────────────────────────────
.nom-body {
padding: 20px;
overflow: visible;
position: relative;
z-index: 10;
}
// ─── Manual form grid ──────────────────────────────────────────
.nom-form {
display: flex;
flex-direction: column;
gap: 0;
}
.nom-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.nom-field {
width: 100%;
&--full {
width: 100%;
display: block;
}
}
.nom-section-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: $blue;
margin: 8px 0 4px;
i {
font-size: 12px;
}
}
.nom-hint {
font-size: 10px;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: $muted;
margin-left: 2px;
}
.nom-flags {
display: flex;
align-items: center;
gap: 24px;
margin: 8px 0 12px;
}
// ─── Search (link mode) ────────────────────────────────────────
.nom-search-wrap {
position: relative;
z-index: 1000;
overflow: visible;
}
.nom-results {
margin-top: 4px;
border: 1px solid $border;
border-radius: 8px;
background: #fff;
box-shadow: 0 4px 20px rgb(0 0 0 / 10%);
overflow: hidden;
max-height: 260px;
overflow-y: auto;
&__loading {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
font-size: 13px;
color: $muted;
}
}
.nom-result-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid $border;
&:last-child {
border-bottom: none;
}
&:hover {
background: $blue-light;
}
&__icon {
color: $muted;
font-size: 20px;
width: 20px;
height: 20px;
flex: 0 0 auto;
}
&__info {
display: flex;
flex-direction: column;
gap: 2px;
}
&__name {
font-size: 13px;
font-weight: 600;
color: $text;
}
&__meta {
font-size: 11px;
color: $muted;
}
}
.nom-no-results {
padding: 12px 0;
font-size: 13px;
color: $muted;
text-align: center;
}
// ─── Selected client card ──────────────────────────────────────
.nom-client-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid $border;
border-radius: 10px;
background: $gray-bg;
margin-bottom: 14px;
&__avatar {
width: 46px;
height: 46px;
border-radius: 50%;
background: $blue-light;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
overflow: hidden;
}
&__photo {
width: 100%;
height: 100%;
object-fit: cover;
}
&__avatar-icon {
color: $blue;
font-size: 26px;
width: 26px;
height: 26px;
}
&__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
&__name {
font-size: 14px;
font-weight: 700;
color: $text;
}
&__meta {
font-size: 12px;
color: $muted;
}
&__remove {
flex: 0 0 auto;
color: $muted;
}
}
.nom-fetching {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: $muted;
padding: 6px 0;
}
// ─── Linked client details grid ────────────────────────────────
.nom-details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px 24px;
padding: 14px 16px;
border: 1px solid $border;
border-radius: 10px;
background: #fff;
margin-bottom: 14px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.nom-detail {
display: flex;
flex-direction: column;
gap: 4px;
&__label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
color: $muted;
text-transform: uppercase;
}
&__value {
font-size: 13px;
font-weight: 600;
color: $text;
display: flex;
align-items: center;
gap: 6px;
}
}
.nom-verified {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-weight: 600;
color: $green;
background: $green-light;
border-radius: 999px;
padding: 2px 8px;
mat-icon {
font-size: 12px;
width: 12px;
height: 12px;
}
}
// ─── Extras for linked (relationship + share) ──────────────────
.nom-linked-extras {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 4px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,254 @@
/**
* 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, Output, EventEmitter, inject, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms';
import { DomSanitizer } from '@angular/platform-browser';
import { ClientsService } from 'app/clients/clients.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
export interface NomineeData {
mode: 'manual' | 'linked';
// Manual fields
fullName?: string;
relationship?: string;
dateOfBirth?: string;
nationality?: string;
aadhaarNumber?: string;
panNumber?: string;
mobileNo?: string;
nomineeShare?: number;
isAlive?: boolean;
isDependent?: boolean;
guardianName?: string;
guardianRelationship?: string;
remarks?: string;
// Linked client fields
clientId?: number;
displayName?: string;
}
const RELATIONSHIPS = [
'Spouse',
'Son',
'Daughter',
'Father',
'Mother',
'Brother',
'Sister',
'Grandfather',
'Grandmother',
'Grandson',
'Granddaughter',
'Uncle',
'Aunt',
'Nephew',
'Niece',
'Friend',
'Other'
];
@Component({
selector: 'mifosx-nominee-form',
templateUrl: './nominee-form.component.html',
styleUrls: ['./nominee-form.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
MatProgressSpinner,
MatIconButton,
MatIcon,
MatCheckbox,
MatDatepickerModule
]
})
export class NomineeFormComponent implements OnInit {
private fb = inject(UntypedFormBuilder);
private clientsService = inject(ClientsService);
private sanitizer = inject(DomSanitizer);
@Output() nomineeChange = new EventEmitter<NomineeData | null>();
mode: 'manual' | 'linked' = 'manual';
relationships = RELATIONSHIPS;
// Manual form
manualForm!: UntypedFormGroup;
minDate = new Date(1900, 0, 1);
maxDate = new Date();
// Link mode
searchText = '';
searching = false;
searchResults: any[] = [];
selectedClient: any = null;
fetchingLinkedData = false;
linkedProfileImage: any = null;
linkedKycStatus: any = null;
clientDob = '';
clientAadhaar = '';
clientPan = '';
linkedRelationship = '';
linkedShare = 100;
private searchTimer: any;
ngOnInit(): void {
this.manualForm = this.fb.group({
fullName: [
'',
Validators.required
],
relationship: [
'',
Validators.required
],
dateOfBirth: [''],
nationality: ['Indian'],
aadhaarNumber: [''],
panNumber: [''],
mobileNo: [''],
nomineeShare: [100],
isAlive: [true],
isDependent: [false],
guardianName: [''],
guardianRelationship: [''],
remarks: ['']
});
this.manualForm.valueChanges.subscribe(() => {
if (this.mode === 'manual' && this.manualForm.valid) {
this.emitManual();
}
});
}
switchMode(m: 'manual' | 'linked'): void {
this.mode = m;
if (m === 'manual') {
this.clearLinked();
} else {
this.manualForm.reset({ nationality: 'Indian', isAlive: true, isDependent: false, nomineeShare: 100 });
this.nomineeChange.emit(null);
}
}
// ─── Manual ──────────────────────────────────────
private emitManual(): void {
const v = this.manualForm.value;
this.nomineeChange.emit({ mode: 'manual', ...v });
}
// ─── Link existing client ─────────────────────────
onSearch(text: string): void {
this.searchText = text;
clearTimeout(this.searchTimer);
if (!text || text.length < 2) {
this.searchResults = [];
return;
}
this.searching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8).subscribe({
next: (data: any) => {
this.searchResults = data?.content || [];
this.searching = false;
},
error: () => {
this.searchResults = [];
this.searching = false;
}
});
}, 400);
}
selectClient(client: any): void {
this.selectedClient = client;
this.searchResults = [];
this.searchText = '';
this.fetchLinkedClientData(client.id);
this.nomineeChange.emit({ mode: 'linked', clientId: client.id, displayName: client.displayName });
}
removeLinked(): void {
this.clearLinked();
this.nomineeChange.emit(null);
}
onLinkedRelationshipChange(rel: string): void {
if (this.selectedClient) {
this.nomineeChange.emit({
mode: 'linked',
clientId: this.selectedClient.id,
displayName: this.selectedClient.displayName,
relationship: rel,
nomineeShare: this.linkedShare
});
}
}
private clearLinked(): void {
this.selectedClient = null;
this.linkedProfileImage = null;
this.linkedKycStatus = null;
this.clientDob = '';
this.clientAadhaar = '';
this.clientPan = '';
this.searchText = '';
this.searchResults = [];
this.linkedRelationship = '';
this.linkedShare = 100;
}
private fetchLinkedClientData(clientId: string): void {
this.fetchingLinkedData = true;
this.clientsService.getClientData(clientId).subscribe({
next: (data: any) => {
if (data.dateOfBirth) {
const d = Array.isArray(data.dateOfBirth)
? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2])
: new Date(data.dateOfBirth);
this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
this.fetchingLinkedData = false;
},
error: () => {
this.fetchingLinkedData = false;
}
});
this.clientsService.getClientKyc(clientId).subscribe({
next: (kyc: any) => {
if (kyc) {
this.clientAadhaar = kyc.aadhaarNumberMasked || '';
this.clientPan = kyc.panNumber || '';
this.linkedKycStatus = {
panVerified: kyc.panVerified || false,
aadhaarVerified: kyc.aadhaarVerified || false
};
}
},
error: () => {}
});
this.clientsService.getClientProfileImage(clientId).subscribe({
next: (imageData: any) => {
if (imageData) {
this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData);
}
},
error: () => {}
});
}
}
+2 -3
View File
@@ -5,13 +5,12 @@
* 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/.
*/
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
/* tslint:disable */
export default {
'mifos_x': {
'version': '260422',
'hash': 'e9a93771'
'version': '260424',
'hash': 'fc1c5126'
},
'allow_switching_backend_instance': true
};