Compare commits

...
78 changed files with 12711 additions and 2 deletions
@@ -11,6 +11,10 @@ import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service'; import { Route } from '../core/route/route.service';
import { AccountsListComponent } from './accounts-list/accounts-list.component'; import { AccountsListComponent } from './accounts-list/accounts-list.component';
import { PartyMasterComponent } from '../deposits/party-master/party-master.component';
import { DailyTransactionEntryComponent } from '../deposits/daily-transaction-entry/daily-transaction-entry.component';
import { DdsReportsComponent } from '../deposits/dds-reports/dds-reports.component';
import { ReportViewerComponent } from '../deposits/dds-reports/report-viewer/report-viewer.component';
const routes: Routes = [ const routes: Routes = [
Route.withShell([ Route.withShell([
@@ -41,6 +45,28 @@ const routes: Routes = [
title: 'Recurring Deposit Accounts', title: 'Recurring Deposit Accounts',
breadcrumb: 'Recurring Deposit Accounts' breadcrumb: 'Recurring Deposit Accounts'
} }
},
{
path: 'dds',
component: PartyMasterComponent,
data: { title: 'Daily Deposit Scheme — Party Master', breadcrumb: 'Party Master' }
},
{
path: 'dds-collection',
component: DailyTransactionEntryComponent,
data: { title: 'Daily Transaction Entry', breadcrumb: 'Daily Transaction Entry' }
},
{
path: 'dds-reports',
data: { title: 'DDS Reports', breadcrumb: 'DDS Reports' },
children: [
{ path: '', component: DdsReportsComponent },
{
path: 'view/:reportName',
component: ReportViewerComponent,
data: { title: 'DDS Report', breadcrumb: 'reportName', routeParamBreadcrumb: 'reportName' }
}
]
} }
] ]
} }
+7 -2
View File
@@ -1048,14 +1048,16 @@
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/recurring-deposits']"> <button mat-menu-item tabindex="0" [routerLink]="['/accounts/recurring-deposits']">
<fa-icon icon="sync" class="menu-item-icon"></fa-icon> Recurring <fa-icon icon="sync" class="menu-item-icon"></fa-icon> Recurring
</button> </button>
<button mat-menu-item tabindex="0"><fa-icon icon="coins" class="menu-item-icon"></fa-icon> DDS</button> <button mat-menu-item tabindex="0" [routerLink]="['/accounts/dds']">
<fa-icon icon="coins" class="menu-item-icon"></fa-icon> DDS
</button>
</div> </div>
<div class="deposit-menu-group"> <div class="deposit-menu-group">
<div class="deposit-group-title">Agent</div> <div class="deposit-group-title">Agent</div>
<button mat-menu-item tabindex="0"> <button mat-menu-item tabindex="0">
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Agent Commission <fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Agent Commission
</button> </button>
<button mat-menu-item tabindex="0"> <button mat-menu-item tabindex="0" [routerLink]="['/accounts/dds-collection']">
<fa-icon icon="calendar-check" class="menu-item-icon"></fa-icon> Today Collection <fa-icon icon="calendar-check" class="menu-item-icon"></fa-icon> Today Collection
</button> </button>
<button mat-menu-item tabindex="0"> <button mat-menu-item tabindex="0">
@@ -1070,6 +1072,9 @@
</div> </div>
<div class="deposit-menu-group"> <div class="deposit-menu-group">
<div class="deposit-group-title">Reports</div> <div class="deposit-group-title">Reports</div>
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/dds-reports']">
<fa-icon icon="coins" class="menu-item-icon"></fa-icon> DDS Reports
</button>
<button mat-menu-item tabindex="0"> <button mat-menu-item tabindex="0">
<fa-icon icon="clock" class="menu-item-icon"></fa-icon> Account Expiry <fa-icon icon="clock" class="menu-item-icon"></fa-icon> Account Expiry
</button> </button>
@@ -0,0 +1,241 @@
<!--
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="dte-container">
<div class="dte-header">
<button mat-stroked-button [routerLink]="['../']" class="dte-back">
<i class="fas fa-arrow-left"></i> Deposits
</button>
<h2 class="dte-header__title">Daily Transaction Detail</h2>
</div>
<div class="dte-card">
<form [formGroup]="entryForm">
<div class="dte-info-panels">
<div class="dte-info-panel">
<div class="dte-info-panel__title">Agent Information</div>
<dl>
<dt>Name</dt>
<dd>{{ agentInfo.name || '—' }}</dd>
<dt>Address</dt>
<dd>{{ agentInfo.address || '—' }}</dd>
<dt>City</dt>
<dd>{{ agentInfo.city || '—' }}</dd>
<dt>A/C</dt>
<dd>{{ agentInfo.accountNo || '—' }}</dd>
<dt>Guarantor</dt>
<dd>{{ agentInfo.guarantor1 || '—' }}</dd>
<dt>Guarantor</dt>
<dd>{{ agentInfo.guarantor2 || '—' }}</dd>
<dt>Curr Bal</dt>
<dd>{{ agentInfo.currentBalance | number: '1.2-2' }}</dd>
</dl>
</div>
<div class="dte-info-panel">
<div class="dte-info-panel__title">Party Information</div>
<p class="dte-info-panel__hint">Populated once a party is selected in a collection row below.</p>
</div>
</div>
<div class="dte-controls">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Agent</mat-label>
<input matInput required formControlName="agentId" />
</mat-form-field>
<button mat-stroked-button type="button" (click)="loadAgent()"><i class="fas fa-search"></i> Load</button>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Date</mat-label>
<input matInput required [matDatepicker]="txnDatePicker" formControlName="transactionDate" />
<mat-datepicker-toggle matIconSuffix [for]="txnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #txnDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Tran. Type</mat-label>
<mat-select required formControlName="transactionType">
@for (type of transactionTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (isPartyTran) {
<div class="dte-section">
<div class="dte-section__header">
<i class="fas fa-list"></i>
<h3>Today's Collections</h3>
<button mat-stroked-button type="button" class="dte-add-row" (click)="addCollectionRow()">
<i class="fas fa-plus"></i> Add Row
</button>
</div>
@if (collections.length === 0) {
<p class="dte-empty">No collection rows added yet.</p>
}
@for (row of collections.controls; track row; let i = $index) {
<div class="dte-collection-row" [formGroup]="$any(row)">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Party Code</mat-label>
<input matInput required formControlName="partyCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>TRTP</mat-label>
<mat-select required formControlName="transactionMode">
@for (mode of transactionModes; track mode) {
<mat-option [value]="mode">{{ mode }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>AC Type</mat-label>
<input matInput formControlName="accountType" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>A/C No.</mat-label>
<input matInput required formControlName="accountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Amount</mat-label>
<input matInput required type="number" formControlName="amount" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Cheque No</mat-label>
<input matInput formControlName="chequeNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Token No</mat-label>
<input matInput formControlName="tokenNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field dte-field--wide">
<mat-label>Description</mat-label>
<input matInput formControlName="description" />
</mat-form-field>
<button
mat-icon-button
type="button"
class="dte-remove-row"
(click)="removeCollectionRow(i)"
matTooltip="Remove row"
>
<i class="fas fa-trash"></i>
</button>
</div>
}
</div>
}
@if (isAgentTran) {
<div class="dte-section">
<div class="dte-section__header">
<i class="fas fa-hand-holding-usd"></i>
<h3>Agent Settlement</h3>
</div>
<p class="dte-info-panel__hint">
Cash the agent hands over to the branch, deducted from the agent's collected balance shown above.
</p>
<div class="dte-grid">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Amount</mat-label>
<input matInput required type="number" formControlName="settlementAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>TRTP</mat-label>
<mat-select required formControlName="settlementMode">
@for (mode of transactionModes; track mode) {
<mat-option [value]="mode">{{ mode }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>AC Type</mat-label>
<input matInput formControlName="settlementAccountType" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>A/C No.</mat-label>
<input matInput formControlName="settlementAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field dte-field--wide">
<mat-label>Description</mat-label>
<input matInput formControlName="settlementDescription" />
</mat-form-field>
</div>
</div>
}
@if (isWithdrawDeposite) {
<div class="dte-section">
<div class="dte-section__header">
<i class="fas fa-exchange-alt"></i>
<h3>Withdraw Deposite</h3>
</div>
<div class="dte-grid">
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Party Code</mat-label>
<input matInput formControlName="creditorPartyCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Party Name</mat-label>
<input matInput formControlName="creditorPartyName" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Creditor Curr Bal</mat-label>
<input matInput formControlName="creditorPartyBalance" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Party Code</mat-label>
<input matInput formControlName="debitPartyCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Party Name</mat-label>
<input matInput formControlName="debitPartyName" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Debit Curr Bal</mat-label>
<input matInput formControlName="debitPartyBalance" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field">
<mat-label>Amount</mat-label>
<input matInput type="number" formControlName="withdrawAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="dte-field dte-field--wide">
<mat-label>Description</mat-label>
<input matInput formControlName="withdrawDescription" />
</mat-form-field>
</div>
</div>
}
@if (!isPartyTran && !isWithdrawDeposite && !isAgentTran) {
<div class="dte-section">
<p class="dte-empty">
"{{ entryForm.get('transactionType').value }}" entry screen — same shape as Party Tran, to be filled in once
the backend distinguishes these transaction types.
</p>
</div>
}
<div class="dte-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
<button mat-stroked-button type="button" (click)="retrieve()">Retrieve</button>
<button mat-stroked-button type="button" (click)="viewLedger()">View Ledger</button>
<button mat-stroked-button type="button" (click)="signature()">Signature</button>
<button mat-raised-button color="primary" type="button" [disabled]="entryForm.invalid" (click)="save()">
Save
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,187 @@
/**
* 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/.
*/
$primary: #0f766e;
.dte-container {
max-width: 1080px;
margin: 0 auto;
padding: 24px 16px;
}
.dte-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.dte-back {
color: $primary;
}
.dte-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 20px 24px 24px;
}
.dte-info-panels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.dte-info-panel {
border: 1px solid rgb(0 0 0 / 10%);
border-radius: 8px;
padding: 12px 16px;
background: rgb(15 118 110 / 4%);
&__title {
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: $primary;
margin-bottom: 8px;
}
&__hint {
margin: 0;
color: rgb(0 0 0 / 50%);
font-size: 13px;
}
dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
margin: 0;
}
dt {
font-weight: 500;
color: rgb(0 0 0 / 60%);
font-size: 13px;
}
dd {
margin: 0;
font-size: 13px;
}
}
.dte-controls {
display: flex;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.dte-field {
min-width: 180px;
&--wide {
grid-column: span 2;
}
}
.dte-section {
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
i {
color: $primary;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
flex: 1;
}
}
}
.dte-add-row {
color: $primary;
}
.dte-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0;
}
.dte-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.dte-collection-row {
display: grid;
grid-template-columns: repeat(7, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-top: 1px dashed rgb(0 0 0 / 10%);
&:first-of-type {
border-top: none;
}
}
.dte-collection-row .dte-field--wide {
grid-column: span 2;
}
.dte-remove-row {
color: #b91c1c;
margin-top: 8px;
}
.dte-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
flex-wrap: wrap;
}
@media (width <= 1000px) {
.dte-info-panels {
grid-template-columns: 1fr;
}
.dte-grid {
grid-template-columns: repeat(2, 1fr);
}
.dte-collection-row {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,290 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/**
* Daily Transaction Entry component.
*
* The screen a collection agent uses at the end of a day's round: pick a
* transaction type, then batch-enter that day's collections against
* assigned parties (Party Tran), settle collected cash to the branch
* (Agent Tran), or record a withdrawal/transfer between accounts (Withdraw
* Deposite). Modelled directly on the legacy Daily Transaction Detail
* screen this replaces.
*
* Backed by the DDS Party Transaction API (`/dds/partytransactions`) for
* Party Tran, Agent Tran and Withdraw Deposite - the three types this
* screen actually collects fields for; Party Payment / OD Entry / OD
* Payment remain placeholders until the backend distinguishes them too.
* "Retrieve" (a ready-made list of today's assigned parties) isn't wired -
* that needs a dedicated per-agent Party lookup this MVP doesn't have yet.
*/
@Component({
selector: 'mifosx-daily-transaction-entry',
templateUrl: './daily-transaction-entry.component.html',
styleUrls: ['./daily-transaction-entry.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class DailyTransactionEntryComponent {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private static readonly TRANSACTION_TYPE_CODES: Record<string, string> = {
'Party Tran': 'PARTY_TRAN',
'Agent Tran': 'AGENT_TRAN',
'Withdraw Deposite': 'WITHDRAW_DEPOSITE'
};
transactionTypes = [
'Party Tran',
'Agent Tran',
'Party Payment',
'OD Entry',
'OD Payment',
'Withdraw Deposite'
];
transactionModes = [
'Cash',
'Transfer',
'Clearing'
];
entryForm: UntypedFormGroup = this.formBuilder.group({
agentId: [
'',
Validators.required
],
transactionType: [
'Party Tran',
Validators.required
],
transactionDate: [
new Date(),
Validators.required
],
// Withdraw Deposite fields
creditorPartyCode: [''],
creditorPartyName: [
{
value: '',
disabled: true
}
],
creditorPartyBalance: [
{
value: 0,
disabled: true
}
],
debitPartyCode: [''],
debitPartyName: [
{
value: '',
disabled: true
}
],
debitPartyBalance: [
{
value: 0,
disabled: true
}
],
withdrawAmount: [0],
withdrawDescription: [''],
// Agent Tran fields
settlementAmount: [0],
settlementMode: ['Cash'],
settlementAccountType: [''],
settlementAccountNo: [''],
settlementDescription: [''],
collections: this.formBuilder.array([])
});
agentInfo = {
name: '',
address: '',
city: '',
accountNo: '',
guarantor1: '',
guarantor2: '',
currentBalance: 0
};
get isPartyTran(): boolean {
return this.entryForm.get('transactionType').value === 'Party Tran';
}
get isWithdrawDeposite(): boolean {
return this.entryForm.get('transactionType').value === 'Withdraw Deposite';
}
get isAgentTran(): boolean {
return this.entryForm.get('transactionType').value === 'Agent Tran';
}
get collections(): UntypedFormArray {
return this.entryForm.get('collections') as UntypedFormArray;
}
private buildCollectionRow(): UntypedFormGroup {
return this.formBuilder.group({
partyCode: [
'',
Validators.required
],
transactionMode: [
'Cash',
Validators.required
],
accountType: [''],
accountNo: [
'',
Validators.required
],
amount: [
0,
Validators.required
],
chequeNo: [''],
tokenNo: [''],
description: [''],
partyName: [
{
value: '',
disabled: true
}
],
balanceAfterTransaction: [
{
value: 0,
disabled: true
}
]
});
}
addCollectionRow(): void {
this.collections.push(this.buildCollectionRow());
}
removeCollectionRow(index: number): void {
this.collections.removeAt(index);
}
loadAgent(): void {
const agentId = this.entryForm.get('agentId')?.value;
if (!agentId) {
return;
}
this.organizationService.getAgent(agentId).subscribe((agent: any) => {
const guarantors = agent.guarantors || [];
this.agentInfo = {
name: agent.agentNameId || '',
address: agent.agentAddress1 || '',
city: agent.agentCity || '',
accountNo: agent.agentSavingAccountNo || '',
guarantor1: guarantors[0]?.guarantorName || '',
guarantor2: guarantors[1]?.guarantorName || '',
currentBalance: agent.agentCurrentBalance || 0
};
});
}
retrieve(): void {
// No "my assigned parties" read API exists yet — this will pre-fill
// the collections grid with today's assigned parties once available.
}
viewLedger(): void {
// No ledger view exists yet for this agent.
}
signature(): void {
// No signature capture/verification flow exists yet.
}
cancel(): void {
this.entryForm.reset({
transactionType: 'Party Tran',
transactionDate: new Date()
});
this.collections.clear();
}
exit(): void {
this.router.navigate(['/accounts/savings']);
}
save(): void {
const raw = this.entryForm.getRawValue();
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
const transactionType = DailyTransactionEntryComponent.TRANSACTION_TYPE_CODES[raw.transactionType];
const payload: any = {
agentId: Number(raw.agentId),
transactionType,
transactionDate: this.dateUtils.formatDate(raw.transactionDate, dateFormat),
dateFormat,
locale
};
if (this.isPartyTran) {
payload.collections = (raw.collections || []).map((row: any) => ({
partyCode: row.partyCode,
transactionMode: row.transactionMode,
accountType: row.accountType || undefined,
accountNo: row.accountNo,
amount: row.amount,
chequeNo: row.chequeNo || undefined,
tokenNo: row.tokenNo || undefined,
description: row.description || undefined
}));
} else if (this.isWithdrawDeposite) {
payload.creditorPartyCode = raw.creditorPartyCode;
payload.debitPartyCode = raw.debitPartyCode;
payload.withdrawAmount = raw.withdrawAmount;
payload.withdrawDescription = raw.withdrawDescription || undefined;
} else if (this.isAgentTran) {
payload.amount = raw.settlementAmount;
payload.transactionMode = raw.settlementMode;
payload.accountType = raw.settlementAccountType || undefined;
payload.accountNo = raw.settlementAccountNo || undefined;
payload.description = raw.settlementDescription || undefined;
}
this.organizationService.createPartyTransaction(payload).subscribe(() => {
this.collections.clear();
this.entryForm.patchValue({
creditorPartyCode: '',
debitPartyCode: '',
withdrawAmount: 0,
withdrawDescription: '',
settlementAmount: 0,
settlementAccountType: '',
settlementAccountNo: '',
settlementDescription: ''
});
this.loadAgent();
});
}
}
@@ -0,0 +1,33 @@
<!--
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="dr-container">
<div class="dr-header">
<button mat-stroked-button [routerLink]="['../']" class="dr-back">
<i class="fas fa-arrow-left"></i> Deposits
</button>
<h2 class="dr-header__title">DDS Reports</h2>
</div>
@for (group of reportGroups; track group.title) {
<div class="dr-group">
<div class="dr-group__title">{{ group.title }}</div>
<div class="dr-grid">
@for (report of group.reports; track report) {
<a class="dr-card" [routerLink]="['view', report]">
<div class="dr-card__icon">
<i class="fas fa-file-alt"></i>
</div>
<div class="dr-card__title">{{ report }}</div>
<i class="fas fa-chevron-right dr-card__arrow"></i>
</a>
}
</div>
</div>
}
</div>
@@ -0,0 +1,97 @@
/**
* 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/.
*/
$primary: #0f766e;
.dr-container {
max-width: 1080px;
margin: 0 auto;
padding: 24px 16px;
}
.dr-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.dr-back {
color: $primary;
}
.dr-group {
margin-bottom: 24px;
&__title {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $primary;
font-weight: 700;
margin-bottom: 10px;
}
}
.dr-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.dr-card {
display: flex;
align-items: center;
gap: 12px;
background: var(--md-sys-color-surface, #fff);
border: 1px solid rgb(0 0 0 / 8%);
border-radius: 8px;
padding: 12px 14px;
text-decoration: none;
color: inherit;
transition: box-shadow 0.15s ease;
&:hover {
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
&__icon {
color: $primary;
font-size: 16px;
flex: none;
}
&__title {
flex: 1;
font-size: 13px;
font-weight: 500;
}
&__arrow {
color: rgb(0 0 0 / 30%);
font-size: 12px;
}
}
@media (width <= 900px) {
.dr-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (width <= 600px) {
.dr-grid {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,73 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* DDS Reports hub component.
*
* Lists every DDS report from the legacy Reports menu, grouped the same
* way (As-On-Date, Interest, Daily/Balance/Master, Transaction). Each
* links to the shared report viewer, parameterised by report name.
*
* UI only for now: no report actually queries the backend yet — see
* ReportViewerComponent.
*/
@Component({
selector: 'mifosx-dds-reports',
templateUrl: './dds-reports.component.html',
styleUrls: ['./dds-reports.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class DdsReportsComponent {
reportGroups = [
{
title: 'As On Date',
reports: [
'Supplementary-DDS (As On)',
'Agent Check List',
'Party Check List-As On',
'Party Balance',
'Agent Wise Balance',
'Agent Wise Monthly Collection',
'Commission Register',
'Collection Summary Agent Wise',
'Close Party Ac Register',
'Agent Partywise Daily Collection',
'Daily Collection Balance',
'Interest Certificate',
'Mature Date Wise Report'
]
},
{
title: 'Interest',
reports: [
'Party Int. Register',
'Commission Register'
]
},
{
title: 'Daily / Balance / Master',
reports: [
'Daily Collection Report',
'Balance Summary',
'Master Data Export'
]
},
{
title: 'Transaction',
reports: ['Transaction Register']
},
{
title: 'Statements',
reports: ['Agent Commission Statement']
}
];
}
@@ -0,0 +1,94 @@
<!--
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="rv-container">
<div class="rv-header">
<button mat-stroked-button [routerLink]="['../../']" class="rv-back">
<i class="fas fa-arrow-left"></i> DDS Reports
</button>
<h2 class="rv-header__title">{{ reportName }}</h2>
</div>
<div class="rv-card">
<form [formGroup]="filterForm">
<div class="rv-grid">
<mat-form-field appearance="outline" class="rv-field">
<mat-label>From Date</mat-label>
<input matInput required [matDatepicker]="fromPicker" formControlName="fromDate" />
<mat-datepicker-toggle matIconSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="rv-field">
<mat-label>To Date</mat-label>
<input matInput required [matDatepicker]="toPicker" formControlName="toDate" />
<mat-datepicker-toggle matIconSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="rv-field">
<mat-label>Agent From</mat-label>
<input matInput type="number" formControlName="agentFrom" />
</mat-form-field>
<mat-form-field appearance="outline" class="rv-field">
<mat-label>Agent To</mat-label>
<input matInput type="number" formControlName="agentTo" />
</mat-form-field>
<mat-form-field appearance="outline" class="rv-field">
<mat-label>Party From</mat-label>
<input matInput type="number" formControlName="partyFrom" />
</mat-form-field>
<mat-form-field appearance="outline" class="rv-field">
<mat-label>Party To</mat-label>
<input matInput type="number" formControlName="partyTo" />
</mat-form-field>
</div>
<div class="rv-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-raised-button color="primary" type="button" [disabled]="filterForm.invalid" (click)="runReport()">
<i class="fas fa-play" style="margin-right: 6px"></i> Run Report
</button>
</div>
@if (hasRun) {
<div class="rv-results">
@if (!available) {
<p class="rv-empty">{{ unavailableMessage }}</p>
} @else if (rows.length === 0) {
<p class="rv-empty">No rows matched these filters.</p>
} @else {
<div class="rv-table-scroll">
<table class="rv-table">
<thead>
<tr>
@for (column of columns; track column) {
<th>{{ column }}</th>
}
</tr>
</thead>
<tbody>
@for (row of rows; track row) {
<tr>
@for (column of columns; track column) {
<td>{{ row[column] }}</td>
}
</tr>
}
</tbody>
</table>
</div>
}
</div>
}
</form>
</div>
</div>
@@ -0,0 +1,103 @@
/**
* 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/.
*/
$primary: #0f766e;
.rv-container {
max-width: 1100px;
margin: 0 auto;
padding: 24px 16px;
}
.rv-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.rv-back {
color: $primary;
}
.rv-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 20px 24px 24px;
}
.rv-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.rv-field {
width: 100%;
}
.rv-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
.rv-results {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
.rv-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0;
text-align: center;
padding: 24px 0;
}
.rv-table-scroll {
overflow-x: auto;
}
.rv-table {
width: 100%;
border-collapse: collapse;
white-space: nowrap;
th {
text-align: left;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgb(0 0 0 / 55%);
padding: 8px 10px;
border-bottom: 2px solid rgb(0 0 0 / 10%);
}
td {
padding: 8px 10px;
border-bottom: 1px solid rgb(0 0 0 / 6%);
}
}
@media (width <= 900px) {
.rv-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,120 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { Dates } from 'app/core/utils/dates';
/**
* Generic DDS report viewer.
*
* One shared screen for all 13+ DDS reports — the report name comes from
* the route and is mapped to a backend report key. Every report in the
* legacy system shared the same basic filter shape (date, agent range,
* party range), so this avoids building a bespoke screen per report - the
* backend returns a generic {columns, rows} shape this renders as one
* table regardless of which report it is.
*
* One legacy report (Close Party Ac Register) has no corresponding data in
* the backend yet - Party accounts have no closed/open status at all. Run
* Report still calls through, and the backend's `available: false` +
* message is shown instead of a table.
*/
@Component({
selector: 'mifosx-dds-report-viewer',
templateUrl: './report-viewer.component.html',
styleUrls: ['./report-viewer.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class ReportViewerComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private formBuilder = inject(UntypedFormBuilder);
private organizationService = inject(OrganizationService);
private dateUtils = inject(Dates);
private static readonly REPORT_KEYS: Record<string, string> = {
'Supplementary-DDS (As On)': 'SUPPLEMENTARY_DDS',
'Agent Check List': 'AGENT_CHECK_LIST',
'Party Check List-As On': 'PARTY_CHECK_LIST',
'Party Balance': 'PARTY_BALANCE',
'Agent Wise Balance': 'AGENT_WISE_BALANCE',
'Agent Wise Monthly Collection': 'AGENT_WISE_MONTHLY_COLLECTION',
'Commission Register': 'COMMISSION_REGISTER',
'Collection Summary Agent Wise': 'COLLECTION_SUMMARY_AGENT_WISE',
'Close Party Ac Register': 'CLOSE_PARTY_AC_REGISTER',
'Agent Partywise Daily Collection': 'AGENT_PARTYWISE_DAILY_COLLECTION',
'Daily Collection Balance': 'DAILY_COLLECTION_REPORT',
'Interest Certificate': 'INTEREST_CERTIFICATE',
'Mature Date Wise Report': 'MATURE_DATE_WISE',
'Party Int. Register': 'PARTY_INTEREST_REGISTER',
'Daily Collection Report': 'DAILY_COLLECTION_REPORT',
'Balance Summary': 'BALANCE_SUMMARY',
'Master Data Export': 'MASTER_DATA_EXPORT',
'Transaction Register': 'TRANSACTION_REGISTER',
'Agent Commission Statement': 'AGENT_COMMISSION_STATEMENT'
};
reportName: string = this.route.snapshot.paramMap.get('reportName') || 'Report';
hasRun = false;
available = true;
unavailableMessage = '';
columns: string[] = [];
rows: Array<Record<string, any>> = [];
filterForm: UntypedFormGroup = this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
],
agentFrom: [1],
agentTo: [99999],
partyFrom: [1],
partyTo: [99999999]
});
runReport(): void {
const reportKey = ReportViewerComponent.REPORT_KEYS[this.reportName];
if (!reportKey) {
return;
}
const raw = this.filterForm.getRawValue();
this.organizationService
.runDdsReport(reportKey, {
fromDate: this.dateUtils.getDate(raw.fromDate),
toDate: this.dateUtils.getDate(raw.toDate),
agentFrom: raw.agentFrom,
agentTo: raw.agentTo,
partyFrom: raw.partyFrom,
partyTo: raw.partyTo
})
.subscribe((result: any) => {
this.available = result.available;
this.unavailableMessage = result.message || '';
this.columns = result.columns || [];
this.rows = result.rows || [];
this.hasRun = true;
});
}
exit(): void {
this.router.navigate(['../../'], { relativeTo: this.route });
}
}
@@ -0,0 +1,543 @@
<!--
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="pm-container">
<div class="pm-header">
<button mat-stroked-button [routerLink]="['../']" class="pm-back">
<i class="fas fa-arrow-left"></i> Deposits
</button>
<h2 class="pm-header__title">Party Master Entry</h2>
</div>
<div class="pm-card">
<form [formGroup]="partyForm">
<mat-tab-group class="pm-tabs" [(selectedIndex)]="selectedTabIndex" animationDuration="200ms">
<mat-tab label="Main Detail">
<div class="pm-tab-content">
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-id-card"></i>
<h3>Identity</h3>
</div>
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Client ID</mat-label>
<input matInput required formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Sub Type</mat-label>
<mat-select required formControlName="subType">
@for (type of subTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Agent ID</mat-label>
<input matInput required formControlName="agentId" />
</mat-form-field>
@if (linkedSavingsAccountId) {
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Savings Account</mat-label>
<input matInput [value]="'Linked (id ' + linkedSavingsAccountId + ')'" disabled />
</mat-form-field>
} @else {
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Savings Account No (if any)</mat-label>
<input matInput formControlName="savingsAccountNo" />
</mat-form-field>
}
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Party Code</mat-label>
<input matInput formControlName="partyCode" placeholder="Auto-generated" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Last A/c</mat-label>
<input matInput formControlName="lastAccount" placeholder="—" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Nominal</mat-label>
<mat-select required formControlName="nominal">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
<mat-label>Party Name</mat-label>
<input matInput required formControlName="partyName" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
<mat-label>Party Name 1</mat-label>
<input matInput formControlName="partyName1" />
</mat-form-field>
</div>
</div>
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-map-marker-alt"></i>
<h3>Address &amp; Contact</h3>
</div>
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
<mat-label>Address 1</mat-label>
<input matInput required formControlName="address1" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
<mat-label>Address 2</mat-label>
<input matInput formControlName="address2" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
<mat-label>Address 3</mat-label>
<input matInput formControlName="address3" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>City</mat-label>
<input matInput required formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Pincode</mat-label>
<input matInput required formControlName="pincode" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Gender</mat-label>
<mat-select formControlName="gender">
<mat-option value="Male">Male</mat-option>
<mat-option value="Female">Female</mat-option>
<mat-option value="Other">Other</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Telephone</mat-label>
<input matInput formControlName="telephone" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Mobile No (SMS)</mat-label>
<input matInput required formControlName="mobileNoSms" />
</mat-form-field>
</div>
</div>
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-shield-alt"></i>
<h3>KYC</h3>
</div>
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Aadhar No</mat-label>
<input matInput formControlName="aadharNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>PAN Card No</mat-label>
<input matInput formControlName="panCardNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>CKYC ID</mat-label>
<input matInput formControlName="ckycId" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Operation</mat-label>
<mat-select required formControlName="operation">
@for (op of operations; track op) {
<mat-option [value]="op">{{ op }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Birth Date</mat-label>
<input matInput [matDatepicker]="birthPicker" formControlName="birthDate" />
<mat-datepicker-toggle matIconSuffix [for]="birthPicker"></mat-datepicker-toggle>
<mat-datepicker #birthPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Occupation</mat-label>
<mat-select formControlName="occupation">
@for (occ of occupations; track occ) {
<mat-option [value]="occ">{{ occ }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Risk</mat-label>
<mat-select formControlName="risk">
@for (r of riskLevels; track r) {
<mat-option [value]="r">{{ r }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-user-friends"></i>
<h3>Minor / Guardian</h3>
</div>
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Adult / Minor</mat-label>
<mat-select required formControlName="adultMinor">
@for (opt of adultMinorOptions; track opt) {
<mat-option [value]="opt">{{ opt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Minor Date</mat-label>
<input matInput [matDatepicker]="minorPicker" formControlName="minorDate" />
<mat-datepicker-toggle matIconSuffix [for]="minorPicker"></mat-datepicker-toggle>
<mat-datepicker #minorPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Guardian Client ID</mat-label>
<input matInput formControlName="guardianClientId" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Min. A/c Opt</mat-label>
<input matInput formControlName="minAccountOpt" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Relation</mat-label>
<input matInput formControlName="relation" />
</mat-form-field>
</div>
</div>
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-calendar-day"></i>
<h3>Scheme Terms</h3>
</div>
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Opening Date</mat-label>
<input matInput required [matDatepicker]="openingPicker" formControlName="openingDate" />
<mat-datepicker-toggle matIconSuffix [for]="openingPicker"></mat-datepicker-toggle>
<mat-datepicker #openingPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Month</mat-label>
<input matInput type="number" formControlName="month" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Maturity Date</mat-label>
<input matInput formControlName="maturityDate" placeholder="Calculated" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Reopen Date</mat-label>
<input matInput [matDatepicker]="reopenPicker" formControlName="reopenDate" />
<mat-datepicker-toggle matIconSuffix [for]="reopenPicker"></mat-datepicker-toggle>
<mat-datepicker #reopenPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Deposit Amt</mat-label>
<input matInput type="number" formControlName="depositAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Daily Deposit Amt</mat-label>
<input matInput required type="number" formControlName="dailyDepositAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Days</mat-label>
<input matInput required type="number" formControlName="days" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Ledger No</mat-label>
<input matInput formControlName="ledgerNo" placeholder="Auto-generated" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Mat. Amt</mat-label>
<input matInput formControlName="maturityAmount" placeholder="Calculated" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Interest Rate</mat-label>
<input matInput formControlName="interestRate" placeholder="From product" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Withdraw Type</mat-label>
<mat-select formControlName="withdrawType">
@for (type of withdrawTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-exclamation-triangle"></i>
<h3>Status &amp; Overdue Flags</h3>
</div>
<div class="pm-grid">
<div class="pm-checkbox-field">
<mat-checkbox formControlName="od1Flag">OD1 Flag</mat-checkbox>
</div>
<div class="pm-checkbox-field">
<mat-checkbox formControlName="od2Flag">OD2 Flag</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Lien</mat-label>
<mat-select formControlName="lien">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Pay Date</mat-label>
<input matInput [matDatepicker]="payDatePicker" formControlName="payDate" />
<mat-datepicker-toggle matIconSuffix [for]="payDatePicker"></mat-datepicker-toggle>
<mat-datepicker #payDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Payment Amount</mat-label>
<input matInput type="number" formControlName="paymentAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Current Bal</mat-label>
<input matInput formControlName="currentBalance" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Opening Bal</mat-label>
<input matInput type="number" formControlName="openingBalance" />
</mat-form-field>
<div class="pm-checkbox-field">
<mat-checkbox formControlName="isSmsActive">Is SMS Active</mat-checkbox>
</div>
<div class="pm-checkbox-field">
<mat-checkbox formControlName="isEmailActive">Is Email Active</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Int.Calc</mat-label>
<input matInput formControlName="interestCalc" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Interest Date</mat-label>
<input matInput [matDatepicker]="interestDatePicker" formControlName="interestDate" />
<mat-datepicker-toggle matIconSuffix [for]="interestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #interestDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
<mat-label>Sp.Description</mat-label>
<input matInput formControlName="spDescription" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Fr'z CD</mat-label>
<mat-select formControlName="freezeCode">
@for (code of freezeCodes; track code) {
<mat-option [value]="code">{{ code }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Adv Code</mat-label>
<mat-select formControlName="advCode">
@for (code of advCodes; track code) {
<mat-option [value]="code">{{ code }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
@if (partyForm.get('advCode').value === 'Transfer To Saving') {
<div class="pm-section">
<div class="pm-section__header">
<i class="fas fa-exchange-alt"></i>
<h3>Maturity Payout Routing</h3>
</div>
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Credit To Branch</mat-label>
<input matInput required formControlName="creditToBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Credit To A/C Type</mat-label>
<input matInput required formControlName="creditToAccountType" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Credit To A/C</mat-label>
<input matInput required formControlName="creditToAccount" />
</mat-form-field>
</div>
</div>
}
</div>
</mat-tab>
<mat-tab label="Nominee's Detail">
<div class="pm-tab-content">
<div class="pm-section">
<div class="pm-grid">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Nominee</mat-label>
<mat-select formControlName="nomineeApplicable">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-radio-group formControlName="nomineeSplitMode" class="pm-radio-group">
<mat-radio-button value="Percentage">Percentage</mat-radio-button>
<mat-radio-button value="Sequence">Sequence</mat-radio-button>
</mat-radio-group>
<button mat-stroked-button type="button" class="pm-add-nominee" (click)="addNominee()">
<i class="fas fa-plus"></i> Add Nominee
</button>
</div>
@if (nominees.length === 0) {
<p class="pm-empty">No nominees added yet.</p>
}
@for (nominee of nominees.controls; track nominee; let i = $index) {
<div class="pm-nominee-row" [formGroup]="$any(nominee)">
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Nom.Dt</mat-label>
<input matInput required [matDatepicker]="nomDatePicker" formControlName="nominationDate" />
<mat-datepicker-toggle matIconSuffix [for]="nomDatePicker"></mat-datepicker-toggle>
<mat-datepicker #nomDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Name</mat-label>
<input matInput required formControlName="name" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Mobile No</mat-label>
<input matInput formControlName="mobileNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Birth Dt</mat-label>
<input matInput [matDatepicker]="nomBirthPicker" formControlName="birthDate" />
<mat-datepicker-toggle matIconSuffix [for]="nomBirthPicker"></mat-datepicker-toggle>
<mat-datepicker #nomBirthPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Minor</mat-label>
<mat-select required formControlName="minor">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Relation</mat-label>
<input matInput required formControlName="relation" />
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Gender</mat-label>
<mat-select required formControlName="gender">
<mat-option value="Male">Male</mat-option>
<mat-option value="Female">Female</mat-option>
<mat-option value="Other">Other</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="pm-field">
<mat-label>Guar. Name</mat-label>
<input matInput formControlName="guardianName" />
</mat-form-field>
<button
mat-icon-button
type="button"
class="pm-remove-nominee"
(click)="removeNominee(i)"
matTooltip="Remove nominee"
>
<i class="fas fa-trash"></i>
</button>
</div>
}
</div>
</div>
</mat-tab>
</mat-tab-group>
@if (isClosed) {
<div class="pm-callout">
<i class="fas fa-lock"></i>
<span>This account is closed. No further collections or edits are possible.</span>
</div>
}
<div class="pm-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
<button mat-stroked-button type="button" (click)="retrieve()">Retrieve</button>
@if (partyId && !isClosed) {
<button
mat-stroked-button
type="button"
color="warn"
matTooltip="Balance must be zero before closing"
(click)="closeAccount()"
>
<i class="fas fa-lock" style="margin-right: 6px"></i> Close Account
</button>
}
<button
mat-raised-button
color="primary"
type="button"
[disabled]="partyForm.invalid || isClosed"
(click)="save()"
>
Save
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,164 @@
/**
* 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/.
*/
$primary: #0f766e;
.pm-container {
max-width: 1080px;
margin: 0 auto;
padding: 24px 16px;
}
.pm-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.pm-back {
color: $primary;
}
.pm-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
}
.pm-tabs {
::ng-deep .mat-mdc-tab-body-wrapper {
min-height: 300px;
}
}
.pm-tab-content {
padding: 20px 24px;
}
.pm-section {
padding: 16px 0;
border-bottom: 1px solid rgb(0 0 0 / 8%);
&:last-of-type {
border-bottom: none;
}
&__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
i {
color: $primary;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
flex: 1;
}
}
}
.pm-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px 16px;
align-items: start;
}
.pm-field {
width: 100%;
&--wide {
grid-column: span 2;
}
}
.pm-checkbox-field {
display: flex;
align-items: center;
height: 56px;
}
.pm-radio-group {
display: flex;
align-items: center;
gap: 16px;
height: 56px;
}
.pm-add-nominee {
color: $primary;
height: fit-content;
align-self: center;
}
.pm-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0 0 8px;
}
.pm-nominee-row {
display: grid;
grid-template-columns: repeat(8, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-top: 1px dashed rgb(0 0 0 / 10%);
&:first-of-type {
border-top: none;
}
}
.pm-remove-nominee {
color: #b91c1c;
margin-top: 8px;
}
.pm-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
.pm-callout {
display: flex;
align-items: center;
gap: 10px;
margin: 0 24px;
padding: 12px 16px;
background: rgb(185 28 28 / 8%);
border: 1px solid rgb(185 28 28 / 25%);
border-radius: 6px;
color: #b91c1c;
font-size: 0.9rem;
}
@media (width <= 1000px) {
.pm-grid {
grid-template-columns: repeat(2, 1fr);
}
.pm-nominee-row {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,527 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { MatTabsModule } from '@angular/material/tabs';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatRadioModule } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/**
* Party Master Entry component.
*
* The customer's DDS account — linked to an Agent for daily collection,
* carrying its own scheme terms, KYC, two-stage overdue flags, and a
* nominee list. Modelled directly on the legacy Party Master Entry screen
* this replaces.
*
* Backed by its own DDS Party API (`/dds/parties`). The underlying account
* does not yet reuse the Recurring Deposit account engine (see Product
* Setup) — that linkage is a follow-up once the RD-account-specific fields
* this screen doesn't collect (interest/compounding periods, chart
* selection, ...) have a home. Until then the scheme/balance fields here
* are the Party's own.
*
* "Close Account" (command=close) permanently closes the Party once its
* balance is zero - the backend rejects the command otherwise. A closed
* Party can no longer be edited or receive new Party Tran collections.
*
* savingsAccountNo optionally links this Party to an existing, non-closed real Savings Account of
* the same client (resolved by account number, since that's what an officer has to hand) -
* clarified with the business (2026-08-24) as "link it if the client already has one, otherwise
* leave it". There is no unlink once linked.
*/
@Component({
selector: 'mifosx-party-master',
templateUrl: './party-master.component.html',
styleUrls: ['./party-master.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTabsModule,
MatCheckbox,
MatRadioModule
]
})
export class PartyMasterComponent {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
partyId: number | null = null;
selectedTabIndex = 0;
partyStatus = 'ACTIVE';
linkedSavingsAccountId: number | null = null;
get isClosed(): boolean {
return this.partyStatus === 'CLOSED';
}
subTypes = [
'Daily Deposit Scheme',
'Masik Dhan Varsha Deposit'
];
operations = [
'Self',
'Guardian',
'Joint'
];
occupations = [
'Business',
'Service',
'Agriculture',
'Homemaker',
'Student',
'Retired',
'Other'
];
riskLevels = [
'Low',
'Medium',
'High'
];
adultMinorOptions = [
'Adult',
'Minor'
];
withdrawTypes = [
'Cash',
'Transfer',
'Clearing'
];
freezeCodes = [
'Normal',
'Debit Freeze',
'Credit Freeze',
'Total Freeze'
];
advCodes = [
'None',
'Transfer To Saving',
'Renew'
];
partyForm: UntypedFormGroup = this.formBuilder.group({
clientId: [
'',
Validators.required
],
subType: [
'',
Validators.required
],
agentId: [
'',
Validators.required
],
savingsAccountNo: [''],
lastAccount: [
{
value: '',
disabled: true
}
],
partyCode: [
{
value: '',
disabled: true
}
],
nominal: [
'No',
Validators.required
],
aadharNo: [''],
partyName: [
'',
Validators.required
],
partyName1: [''],
address1: [
'',
Validators.required
],
address2: [''],
address3: [''],
city: [
'',
Validators.required
],
pincode: [
'',
Validators.required
],
gender: [''],
panCardNo: [''],
telephone: [''],
mobileNoSms: [
'',
Validators.required
],
operation: [
'Self',
Validators.required
],
birthDate: [''],
occupation: [''],
risk: [''],
ckycId: [''],
adultMinor: [
'Adult',
Validators.required
],
minorDate: [''],
guardianClientId: [''],
minAccountOpt: [''],
relation: [''],
openingDate: [
'',
Validators.required
],
month: [''],
maturityDate: [
{
value: '',
disabled: true
}
],
reopenDate: [''],
depositAmount: [0],
dailyDepositAmount: [
0,
Validators.required
],
days: [
'',
Validators.required
],
ledgerNo: [
{
value: '',
disabled: true
}
],
maturityAmount: [
{
value: 0,
disabled: true
}
],
interestRate: [
{
value: 0,
disabled: true
}
],
withdrawType: [''],
od1Flag: [
{
value: false,
disabled: true
}
],
od2Flag: [
{
value: false,
disabled: true
}
],
lien: ['No'],
payDate: [''],
paymentAmount: [0],
currentBalance: [
{
value: 0,
disabled: true
}
],
openingBalance: [0],
isSmsActive: [true],
isEmailActive: [false],
interestCalc: [
{
value: 0,
disabled: true
}
],
interestDate: [''],
spDescription: [''],
freezeCode: ['Normal'],
advCode: ['None'],
creditToBranch: [''],
creditToAccountType: [''],
creditToAccount: [''],
nomineeApplicable: ['No'],
nomineeSplitMode: ['Percentage'],
nominees: this.formBuilder.array([])
});
get nominees(): UntypedFormArray {
return this.partyForm.get('nominees') as UntypedFormArray;
}
private buildNomineeGroup(): UntypedFormGroup {
return this.formBuilder.group({
nominationDate: [
'',
Validators.required
],
name: [
'',
Validators.required
],
mobileNo: [''],
birthDate: [''],
minor: [
'No',
Validators.required
],
relation: [
'',
Validators.required
],
gender: [
'',
Validators.required
],
guardianName: ['']
});
}
addNominee(): void {
this.nominees.push(this.buildNomineeGroup());
}
removeNominee(index: number): void {
this.nominees.removeAt(index);
}
retrieve(): void {
const clientId = this.partyForm.get('clientId')?.value;
if (!clientId) {
return;
}
this.organizationService.getParties().subscribe((parties: any[]) => {
const match = (parties || []).find((party) => String(party.clientId) === String(clientId));
if (match) {
this.patchFormFromParty(match);
}
});
}
cancel(): void {
this.partyId = null;
this.partyStatus = 'ACTIVE';
this.linkedSavingsAccountId = null;
this.partyForm.reset();
this.partyForm.patchValue({
nominal: 'No',
operation: 'Self',
adultMinor: 'Adult',
lien: 'No',
isSmsActive: true,
isEmailActive: false,
freezeCode: 'Normal',
advCode: 'None',
nomineeApplicable: 'No',
nomineeSplitMode: 'Percentage',
currentBalance: 0,
maturityAmount: 0,
interestRate: 0
});
this.nominees.clear();
}
exit(): void {
this.router.navigate(['/accounts/savings']);
}
save(): void {
const raw = this.partyForm.getRawValue();
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
const asDate = (value: any) => (value ? this.dateUtils.formatDate(value, dateFormat) : undefined);
const payload: any = {
clientId: Number(raw.clientId),
agentId: Number(raw.agentId),
savingsAccountNo: this.linkedSavingsAccountId ? undefined : raw.savingsAccountNo || undefined,
subType: raw.subType,
lastAccount: raw.lastAccount || undefined,
nominal: raw.nominal === 'Yes',
aadharNo: raw.aadharNo || undefined,
partyName: raw.partyName,
partyName1: raw.partyName1 || undefined,
address1: raw.address1,
address2: raw.address2 || undefined,
address3: raw.address3 || undefined,
city: raw.city,
pincode: raw.pincode,
gender: raw.gender || undefined,
panCardNo: raw.panCardNo || undefined,
telephone: raw.telephone || undefined,
mobileNoSms: raw.mobileNoSms,
operation: raw.operation,
birthDate: asDate(raw.birthDate),
occupation: raw.occupation || undefined,
risk: raw.risk || undefined,
ckycId: raw.ckycId || undefined,
adultMinor: raw.adultMinor,
minorDate: asDate(raw.minorDate),
guardianClientId: raw.guardianClientId ? Number(raw.guardianClientId) : undefined,
minAccountOpt: raw.minAccountOpt || undefined,
relation: raw.relation || undefined,
openingDate: asDate(raw.openingDate),
month: raw.month || undefined,
reopenDate: asDate(raw.reopenDate),
depositAmount: raw.depositAmount || undefined,
dailyDepositAmount: raw.dailyDepositAmount,
days: raw.days,
withdrawType: raw.withdrawType || undefined,
lien: raw.lien === 'Yes',
payDate: asDate(raw.payDate),
paymentAmount: raw.paymentAmount || undefined,
openingBalance: raw.openingBalance || 0,
isSmsActive: !!raw.isSmsActive,
isEmailActive: !!raw.isEmailActive,
interestDate: asDate(raw.interestDate),
spDescription: raw.spDescription || undefined,
freezeCode: raw.freezeCode || undefined,
advCode: raw.advCode || undefined,
creditToBranch: raw.creditToBranch || undefined,
creditToAccountType: raw.creditToAccountType || undefined,
creditToAccount: raw.creditToAccount || undefined,
nomineeApplicable: raw.nomineeApplicable === 'Yes',
nomineeSplitMode: raw.nomineeSplitMode || undefined,
dateFormat,
locale,
nominees: (raw.nominees || []).map((nominee: any) => ({
nominationDate: this.dateUtils.getDate(nominee.nominationDate),
name: nominee.name,
mobileNo: nominee.mobileNo || undefined,
birthDate: nominee.birthDate ? this.dateUtils.getDate(nominee.birthDate) : undefined,
minor: nominee.minor === 'Yes',
relation: nominee.relation,
gender: nominee.gender,
guardianName: nominee.guardianName || undefined
}))
};
if (this.partyId) {
this.organizationService.updateParty(this.partyId, payload).subscribe(() => this.exit());
} else {
this.organizationService.createParty(payload).subscribe(() => this.exit());
}
}
closeAccount(): void {
if (!this.partyId) {
return;
}
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
this.organizationService
.closeParty(this.partyId, {
closeDate: this.dateUtils.formatDate(new Date(), dateFormat),
dateFormat,
locale
})
.subscribe(() => this.exit());
}
private patchFormFromParty(party: any): void {
this.partyId = party.id;
this.partyStatus = party.status || 'ACTIVE';
this.linkedSavingsAccountId = party.savingsAccountId || null;
this.partyForm.patchValue({
clientId: party.clientId,
subType: party.subType,
agentId: party.agentId,
partyCode: party.partyCode,
lastAccount: party.lastAccount,
nominal: party.nominal ? 'Yes' : 'No',
aadharNo: party.aadharNo,
partyName: party.partyName,
partyName1: party.partyName1,
address1: party.address1,
address2: party.address2,
address3: party.address3,
city: party.city,
pincode: party.pincode,
gender: party.gender,
panCardNo: party.panCardNo,
telephone: party.telephone,
mobileNoSms: party.mobileNoSms,
operation: party.operation,
birthDate: party.birthDate ? new Date(party.birthDate) : null,
occupation: party.occupation,
risk: party.risk,
ckycId: party.ckycId,
adultMinor: party.adultMinor,
minorDate: party.minorDate ? new Date(party.minorDate) : null,
guardianClientId: party.guardianClientId,
minAccountOpt: party.minAccountOpt,
relation: party.relation,
openingDate: party.openingDate ? new Date(party.openingDate) : null,
month: party.month,
maturityDate: party.maturityDate,
reopenDate: party.reopenDate ? new Date(party.reopenDate) : null,
depositAmount: party.depositAmount,
dailyDepositAmount: party.dailyDepositAmount,
days: party.days,
ledgerNo: party.ledgerNo,
maturityAmount: party.maturityAmount,
interestRate: party.interestRate,
withdrawType: party.withdrawType,
od1Flag: party.od1Flag,
od2Flag: party.od2Flag,
lien: party.lien ? 'Yes' : 'No',
payDate: party.payDate ? new Date(party.payDate) : null,
paymentAmount: party.paymentAmount,
currentBalance: party.currentBalance,
openingBalance: party.openingBalance,
isSmsActive: party.isSmsActive,
isEmailActive: party.isEmailActive,
interestCalc: party.interestCalc,
interestDate: party.interestDate ? new Date(party.interestDate) : null,
spDescription: party.spDescription,
freezeCode: party.freezeCode,
advCode: party.advCode,
creditToBranch: party.creditToBranch,
creditToAccountType: party.creditToAccountType,
creditToAccount: party.creditToAccount,
nomineeApplicable: party.nomineeApplicable ? 'Yes' : 'No',
nomineeSplitMode: party.nomineeSplitMode
});
this.nominees.clear();
(party.nominees || []).forEach((nominee: any) => {
const group = this.buildNomineeGroup();
group.patchValue({
nominationDate: nominee.nominationDate ? new Date(nominee.nominationDate) : null,
name: nominee.name,
mobileNo: nominee.mobileNo,
birthDate: nominee.birthDate ? new Date(nominee.birthDate) : null,
minor: nominee.minor ? 'Yes' : 'No',
relation: nominee.relation,
gender: nominee.gender,
guardianName: nominee.guardianName
});
this.nominees.push(group);
});
}
}
@@ -0,0 +1,355 @@
<!--
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="am-container">
<div class="am-header">
<button mat-stroked-button [routerLink]="['../']" class="am-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
</button>
<h2 class="am-header__title">Agent Master Entry</h2>
</div>
<div class="am-card">
<form [formGroup]="agentForm">
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-id-card"></i>
<h3>Agent Details</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Client ID</mat-label>
<input matInput required formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Code</mat-label>
<input matInput formControlName="agentCode" placeholder="Auto-generated" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field am-field--wide">
<mat-label>Agent Name ID</mat-label>
<input matInput required formControlName="agentNameId" placeholder="Person or institution name" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field am-field--wide">
<mat-label>Agent Address (Line 1)</mat-label>
<input matInput required formControlName="agentAddress1" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field am-field--wide">
<mat-label>Agent Address (Line 2)</mat-label>
<input matInput formControlName="agentAddress2" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent City</mat-label>
<input matInput required formControlName="agentCity" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent PinCode</mat-label>
<input matInput required formControlName="agentPinCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Telephone</mat-label>
<input matInput formControlName="agentTelephone" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Home Branch</mat-label>
<input matInput required formControlName="homeBranch" />
</mat-form-field>
</div>
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-percent"></i>
<h3>Collection &amp; Commission Settings</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Com. Calculation</mat-label>
<mat-select required formControlName="commissionCalculation">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>TDS</mat-label>
<mat-select required formControlName="tdsApplicable">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>TDS %</mat-label>
<input matInput type="number" formControlName="tdsPercent" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Cur.TDS</mat-label>
<input matInput formControlName="currentTds" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>TDS Amt</mat-label>
<input matInput formControlName="tdsAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Limit</mat-label>
<input matInput type="number" formControlName="limit" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Machine Rent</mat-label>
<input matInput type="number" formControlName="machineRent" />
</mat-form-field>
</div>
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-piggy-bank"></i>
<h3>Settlement &amp; Security Accounts</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Saving Branch</mat-label>
<input matInput required formControlName="savingBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Saving A/C Type</mat-label>
<mat-select required formControlName="agentSavingAccountType">
@for (accountType of agentSavingAccountTypes; track accountType) {
<mat-option [value]="accountType">{{ accountType }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Saving A/C No</mat-label>
<input matInput required formControlName="agentSavingAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Trf Branch</mat-label>
<input matInput formControlName="transferBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Trf.Amt A/C Type</mat-label>
<input matInput formControlName="transferAmountAccountType" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Trf.Amt A/C No</mat-label>
<input matInput formControlName="transferAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Security Branch</mat-label>
<input matInput formControlName="agentSecurityBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Security A/C Type</mat-label>
<mat-select formControlName="agentSecurityAccountType">
@for (accountType of agentSavingAccountTypes; track accountType) {
<mat-option [value]="accountType">{{ accountType }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Security A/C No</mat-label>
<input matInput formControlName="agentSecurityAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Opening Date</mat-label>
<input matInput required [matDatepicker]="openingDatePicker" formControlName="agentOpeningDate" />
<mat-datepicker-toggle matIconSuffix [for]="openingDatePicker"></mat-datepicker-toggle>
<mat-datepicker #openingDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Birth Date</mat-label>
<input
matInput
required
[max]="maxDate"
[matDatepicker]="birthDatePicker"
formControlName="agentBirthDate"
/>
<mat-datepicker-toggle matIconSuffix [for]="birthDatePicker"></mat-datepicker-toggle>
<mat-datepicker #birthDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Current Balance</mat-label>
<input matInput formControlName="agentCurrentBalance" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Opening Balance</mat-label>
<input matInput formControlName="agentOpeningBalance" />
</mat-form-field>
</div>
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-clock"></i>
<h3>Collection Limit History</h3>
<button mat-stroked-button type="button" class="am-add-guarantor" (click)="addLimit()">
<i class="fas fa-plus"></i> Add Limit
</button>
</div>
@if (limits.length === 0) {
<p class="am-empty-guarantors">No limit periods added yet.</p>
}
@for (limitRow of limits.controls; track limitRow; let i = $index) {
<div class="am-limit-row" [formGroup]="$any(limitRow)">
<mat-form-field appearance="outline" class="am-field">
<mat-label>From Date</mat-label>
<input matInput required [matDatepicker]="fromPicker" formControlName="fromDate" />
<mat-datepicker-toggle matIconSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>To Date</mat-label>
<input matInput required [matDatepicker]="toPicker" formControlName="toDate" />
<mat-datepicker-toggle matIconSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Limit</mat-label>
<input matInput required type="number" formControlName="limit" />
</mat-form-field>
<button
mat-icon-button
type="button"
class="am-remove-guarantor"
(click)="removeLimit(i)"
matTooltip="Remove limit period"
>
<i class="fas fa-trash"></i>
</button>
</div>
}
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-user-shield"></i>
<h3>Guarantor's Detail</h3>
<button mat-stroked-button type="button" class="am-add-guarantor" (click)="addGuarantor()">
<i class="fas fa-plus"></i> Add Guarantor
</button>
</div>
@if (guarantors.length === 0) {
<p class="am-empty-guarantors">No guarantors added yet.</p>
}
@for (guarantor of guarantors.controls; track guarantor; let i = $index) {
<div class="am-guarantor-row" [formGroup]="$any(guarantor)">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Client ID</mat-label>
<input matInput required formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Guarantor Name</mat-label>
<input matInput required formControlName="guarantorName" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Address 1</mat-label>
<input matInput required formControlName="address1" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Address 2</mat-label>
<input matInput formControlName="address2" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>City</mat-label>
<input matInput required formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Pin Code</mat-label>
<input matInput required formControlName="pinCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Telephone</mat-label>
<input matInput formControlName="telephone" />
</mat-form-field>
<button
mat-icon-button
type="button"
class="am-remove-guarantor"
(click)="removeGuarantor(i)"
matTooltip="Remove guarantor"
>
<i class="fas fa-trash"></i>
</button>
</div>
}
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-wallet"></i>
<h3>Deductions &amp; Deposits</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Comm</mat-label>
<input matInput formControlName="commission" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Other Comm</mat-label>
<input matInput type="number" formControlName="otherCommission" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Ag.Deposit</mat-label>
<input matInput type="number" formControlName="agentDeposit" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Adv.Festival</mat-label>
<input matInput type="number" formControlName="festivalAdvance" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Other Deduction</mat-label>
<input matInput type="number" formControlName="otherDeduction" />
</mat-form-field>
</div>
</div>
<div class="am-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
<button mat-stroked-button type="button" (click)="retrieve()">Retrieve</button>
<button mat-raised-button color="primary" type="button" [disabled]="agentForm.invalid" (click)="save()">
Save
</button>
</div>
</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/.
*/
$primary: #0f766e;
.am-container {
max-width: 1040px;
margin: 0 auto;
padding: 24px 16px;
}
.am-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.am-back {
color: $primary;
}
.am-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 8px 24px 24px;
}
.am-section {
padding: 20px 0;
border-bottom: 1px solid rgb(0 0 0 / 8%);
&:last-of-type {
border-bottom: none;
}
&__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
i {
color: $primary;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
flex: 1;
}
}
}
.am-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.am-field {
width: 100%;
&--wide {
grid-column: span 2;
}
}
.am-add-guarantor {
color: $primary;
}
.am-empty-guarantors {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0 0 8px;
}
.am-guarantor-row {
display: grid;
grid-template-columns: repeat(7, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-top: 1px dashed rgb(0 0 0 / 10%);
&:first-of-type {
border-top: none;
}
}
.am-limit-row {
display: grid;
grid-template-columns: repeat(3, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-top: 1px dashed rgb(0 0 0 / 10%);
&:first-of-type {
border-top: none;
}
}
.am-remove-guarantor {
color: #b91c1c;
margin-top: 8px;
}
.am-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
flex-wrap: wrap;
}
@media (width <= 900px) {
.am-grid {
grid-template-columns: repeat(2, 1fr);
}
.am-guarantor-row,
.am-limit-row {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,381 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/**
* Agent Master Entry component.
*
* A DDS collection agent — either a person or an institution — registered
* with its own settlement savings account and one or more guarantors. As of
* 2026-08-24 the Agent also does the daily collection work directly (the
* SubAgent tier was removed): it carries the collection-limit history,
* TDS/commission settings and security account SubAgent used to own.
*
* Backed by its own DDS Agent API (`/dds/agents`) - independent of Staff/
* Client CRUD, per the DDS agent/commission hierarchy being genuinely new
* feature work.
*/
@Component({
selector: 'mifosx-agent-master',
templateUrl: './agent-master.component.html',
styleUrls: ['./agent-master.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class AgentMasterComponent {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
maxDate = new Date();
agentId: number | null = null;
agentSavingAccountTypes = [
'Savings Deposit Individual A/C',
'Savings Deposit Joint A/C',
'Current Account'
];
agentForm: UntypedFormGroup = this.formBuilder.group({
clientId: [
'',
Validators.required
],
agentCode: [
{
value: '',
disabled: true
}
],
agentNameId: [
'',
Validators.required
],
agentAddress1: [
'',
Validators.required
],
agentAddress2: [''],
agentCity: [
'',
Validators.required
],
agentPinCode: [
'',
Validators.required
],
agentTelephone: [''],
agentSavingAccountType: [
'',
Validators.required
],
agentSavingAccountNo: [
'',
Validators.required
],
agentOpeningDate: [
'',
Validators.required
],
agentBirthDate: [
'',
Validators.required
],
agentCurrentBalance: [
{
value: 0,
disabled: true
}
],
agentOpeningBalance: [
{
value: 0,
disabled: true
}
],
homeBranch: [
'',
Validators.required
],
tdsApplicable: [
'No',
Validators.required
],
tdsPercent: [0],
currentTds: [
{
value: 0,
disabled: true
}
],
tdsAmount: [
{
value: 0,
disabled: true
}
],
savingBranch: [
'',
Validators.required
],
transferBranch: [''],
transferAmountAccountType: [''],
transferAccountNo: [''],
commissionCalculation: [
'Yes',
Validators.required
],
limit: [0],
machineRent: [0],
agentSecurityBranch: [''],
agentSecurityAccountType: [''],
agentSecurityAccountNo: [''],
commission: [
{
value: 0,
disabled: true
}
],
otherCommission: [0],
agentDeposit: [0],
festivalAdvance: [0],
otherDeduction: [0],
limits: this.formBuilder.array([]),
guarantors: this.formBuilder.array([])
});
get limits(): UntypedFormArray {
return this.agentForm.get('limits') as UntypedFormArray;
}
get guarantors(): UntypedFormArray {
return this.agentForm.get('guarantors') as UntypedFormArray;
}
private buildLimitGroup(): UntypedFormGroup {
return this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
],
limit: [
0,
Validators.required
]
});
}
private buildGuarantorGroup(): UntypedFormGroup {
return this.formBuilder.group({
clientId: [
'',
Validators.required
],
guarantorName: [
'',
Validators.required
],
address1: [
'',
Validators.required
],
address2: [''],
city: [
'',
Validators.required
],
pinCode: [
'',
Validators.required
],
telephone: ['']
});
}
addLimit(): void {
this.limits.push(this.buildLimitGroup());
}
removeLimit(index: number): void {
this.limits.removeAt(index);
}
addGuarantor(): void {
this.guarantors.push(this.buildGuarantorGroup());
}
removeGuarantor(index: number): void {
this.guarantors.removeAt(index);
}
retrieve(): void {
const clientId = this.agentForm.get('clientId')?.value;
if (!clientId) {
return;
}
this.organizationService.getAgents().subscribe((agents: any[]) => {
const match = (agents || []).find((agent) => String(agent.clientId) === String(clientId));
if (match) {
this.patchFormFromAgent(match);
}
});
}
cancel(): void {
this.agentId = null;
this.agentForm.reset();
this.agentForm.patchValue({
agentCurrentBalance: 0,
agentOpeningBalance: 0,
tdsApplicable: 'No',
commissionCalculation: 'Yes',
currentTds: 0,
tdsAmount: 0,
commission: 0
});
this.limits.clear();
this.guarantors.clear();
}
exit(): void {
this.router.navigate(['/organization']);
}
save(): void {
const raw = this.agentForm.getRawValue();
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
const payload: any = {
clientId: Number(raw.clientId),
agentNameId: raw.agentNameId,
agentAddress1: raw.agentAddress1,
agentAddress2: raw.agentAddress2 || undefined,
agentCity: raw.agentCity,
agentPinCode: raw.agentPinCode,
agentTelephone: raw.agentTelephone || undefined,
agentSavingAccountType: raw.agentSavingAccountType,
agentSavingAccountNo: raw.agentSavingAccountNo,
agentOpeningDate: this.dateUtils.formatDate(raw.agentOpeningDate, dateFormat),
agentBirthDate: this.dateUtils.formatDate(raw.agentBirthDate, dateFormat),
agentOpeningBalance: raw.agentOpeningBalance || 0,
homeBranch: raw.homeBranch,
tdsApplicable: raw.tdsApplicable === 'Yes',
tdsPercent: raw.tdsPercent || undefined,
savingBranch: raw.savingBranch,
transferBranch: raw.transferBranch || undefined,
transferAmountAccountType: raw.transferAmountAccountType || undefined,
transferAccountNo: raw.transferAccountNo || undefined,
commissionCalculation: raw.commissionCalculation === 'Yes',
limit: raw.limit || undefined,
machineRent: raw.machineRent || undefined,
agentSecurityBranch: raw.agentSecurityBranch || undefined,
agentSecurityAccountType: raw.agentSecurityAccountType || undefined,
agentSecurityAccountNo: raw.agentSecurityAccountNo || undefined,
otherCommission: raw.otherCommission || undefined,
agentDeposit: raw.agentDeposit || undefined,
festivalAdvance: raw.festivalAdvance || undefined,
otherDeduction: raw.otherDeduction || undefined,
dateFormat,
locale,
limits: (raw.limits || []).map((limit: any) => ({
fromDate: this.dateUtils.getDate(limit.fromDate),
toDate: this.dateUtils.getDate(limit.toDate),
limit: limit.limit
})),
guarantors: (raw.guarantors || []).map((guarantor: any) => ({
clientId: Number(guarantor.clientId),
guarantorName: guarantor.guarantorName,
address1: guarantor.address1,
address2: guarantor.address2 || undefined,
city: guarantor.city,
pinCode: guarantor.pinCode,
telephone: guarantor.telephone || undefined
}))
};
if (this.agentId) {
this.organizationService.updateAgent(this.agentId, payload).subscribe(() => this.exit());
} else {
this.organizationService.createAgent(payload).subscribe(() => this.exit());
}
}
private patchFormFromAgent(agent: any): void {
this.agentId = agent.id;
this.agentForm.patchValue({
clientId: agent.clientId,
agentCode: agent.agentCode,
agentNameId: agent.agentNameId,
agentAddress1: agent.agentAddress1,
agentAddress2: agent.agentAddress2,
agentCity: agent.agentCity,
agentPinCode: agent.agentPinCode,
agentTelephone: agent.agentTelephone,
agentSavingAccountType: agent.agentSavingAccountType,
agentSavingAccountNo: agent.agentSavingAccountNo,
agentOpeningDate: agent.agentOpeningDate ? new Date(agent.agentOpeningDate) : null,
agentBirthDate: agent.agentBirthDate ? new Date(agent.agentBirthDate) : null,
agentCurrentBalance: agent.agentCurrentBalance,
agentOpeningBalance: agent.agentOpeningBalance,
homeBranch: agent.homeBranch,
tdsApplicable: agent.tdsApplicable ? 'Yes' : 'No',
tdsPercent: agent.tdsPercent,
currentTds: agent.currentTds,
tdsAmount: agent.tdsAmount,
savingBranch: agent.savingBranch,
transferBranch: agent.transferBranch,
transferAmountAccountType: agent.transferAmountAccountType,
transferAccountNo: agent.transferAccountNo,
commissionCalculation: agent.commissionCalculation ? 'Yes' : 'No',
limit: agent.limit,
machineRent: agent.machineRent,
agentSecurityBranch: agent.agentSecurityBranch,
agentSecurityAccountType: agent.agentSecurityAccountType,
agentSecurityAccountNo: agent.agentSecurityAccountNo,
commission: agent.commission,
otherCommission: agent.otherCommission,
agentDeposit: agent.agentDeposit,
festivalAdvance: agent.festivalAdvance,
otherDeduction: agent.otherDeduction
});
this.limits.clear();
(agent.limits || []).forEach((limit: any) => {
const group = this.buildLimitGroup();
group.patchValue({
fromDate: limit.fromDate ? new Date(limit.fromDate) : null,
toDate: limit.toDate ? new Date(limit.toDate) : null,
limit: limit.limit
});
this.limits.push(group);
});
this.guarantors.clear();
(agent.guarantors || []).forEach((guarantor: any) => {
const group = this.buildGuarantorGroup();
group.patchValue(guarantor);
this.guarantors.push(group);
});
}
}
@@ -0,0 +1,93 @@
<!--
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="csm-container">
<div class="csm-header">
<button mat-stroked-button [routerLink]="['../']" class="csm-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
</button>
<h2 class="csm-header__title">Commission Slab Master</h2>
</div>
<div class="csm-card">
<table class="csm-table">
<thead>
<tr>
<th>Slab Type</th>
<th>Slab From</th>
<th>Slab To</th>
<th>Comm Per</th>
<th></th>
</tr>
</thead>
<tbody>
@for (slab of slabs; track slab; let i = $index) {
<tr>
<td>{{ slab.slabType }}</td>
<td>{{ slab.slabFrom }}</td>
<td>{{ slab.slabTo }}</td>
<td>{{ slab.commissionPercent }}%</td>
<td class="csm-row-actions">
<button mat-icon-button type="button" (click)="removeSlab(i)" matTooltip="Remove slab">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
<tr [formGroup]="newSlabForm" class="csm-new-row">
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<mat-select required formControlName="slabType" placeholder="Slab Type">
@for (type of slabTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
</td>
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<input matInput required type="number" formControlName="slabFrom" />
</mat-form-field>
</td>
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<input matInput required type="number" formControlName="slabTo" />
</mat-form-field>
</td>
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<input matInput required type="number" formControlName="commissionPercent" />
</mat-form-field>
</td>
<td class="csm-row-actions">
<button
mat-icon-button
color="primary"
type="button"
[disabled]="newSlabForm.invalid"
(click)="addSlab()"
matTooltip="Add slab"
>
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
</tbody>
</table>
@if (slabs.length === 0) {
<p class="csm-empty">No commission slabs defined yet — add one using the row above.</p>
}
<div class="csm-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-raised-button color="primary" type="button" (click)="save()">Save</button>
</div>
</div>
</div>
@@ -0,0 +1,92 @@
/**
* 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/.
*/
$primary: #0f766e;
.csm-container {
max-width: 760px;
margin: 0 auto;
padding: 24px 16px;
}
.csm-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.csm-back {
color: $primary;
}
.csm-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 16px 24px 24px;
}
.csm-table {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgb(0 0 0 / 55%);
padding: 8px 10px;
border-bottom: 2px solid rgb(0 0 0 / 10%);
}
td {
padding: 6px 10px;
border-bottom: 1px solid rgb(0 0 0 / 6%);
vertical-align: middle;
}
}
.csm-inline-field {
width: 100%;
::ng-deep .mat-mdc-text-field-wrapper {
height: 44px;
}
}
.csm-row-actions {
text-align: center;
width: 48px;
}
.csm-new-row {
background: rgb(15 118 110 / 4%);
}
.csm-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 12px 0 0;
}
.csm-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
@@ -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/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
/**
* Commission Slab Master component.
*
* Two kinds of slab: ACCOUNT (commission percentage on collection volume)
* and COMMISSION (a senior-agent override on another agent's commission -
* deferred: there is no agent hierarchy modelled yet, see the DDS punch
* list). Modelled directly on the legacy Commission Slab Master screen this
* replaces.
*
* Backed by its own DDS Commission Slab API (`/dds/commissionslabs`) - an
* append-only chart, same as the legacy screen: existing rows load on open,
* newly-added rows post on Save, and there is no edit/delete of a
* previously-saved row.
*/
@Component({
selector: 'mifosx-commission-slab-master',
templateUrl: './commission-slab-master.component.html',
styleUrls: ['./commission-slab-master.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class CommissionSlabMasterComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private static readonly SLAB_TYPE_CODES: Record<string, string> = {
Account: 'ACCOUNT',
Commission: 'COMMISSION'
};
private static readonly SLAB_TYPE_LABELS: Record<string, string> = {
ACCOUNT: 'Account',
COMMISSION: 'Commission'
};
slabTypes = [
'Account',
'Commission'
];
displayedColumns: string[] = [
'slabType',
'slabFrom',
'slabTo',
'commissionPercent',
'actions'
];
slabs: Array<{ id?: number; slabType: string; slabFrom: number; slabTo: number; commissionPercent: number }> = [];
newSlabForm: UntypedFormGroup = this.formBuilder.group({
slabType: [
'',
Validators.required
],
slabFrom: [
0,
Validators.required
],
slabTo: [
0,
Validators.required
],
commissionPercent: [
0,
Validators.required
]
});
ngOnInit(): void {
this.loadSlabs();
}
private loadSlabs(): void {
this.organizationService.getCommissionSlabs().subscribe((slabs: any[]) => {
this.slabs = (slabs || []).map((slab) => ({
id: slab.id,
slabType: CommissionSlabMasterComponent.SLAB_TYPE_LABELS[slab.slabType] || slab.slabType,
slabFrom: slab.slabFrom,
slabTo: slab.slabTo,
commissionPercent: slab.commissionPercent
}));
});
}
addSlab(): void {
if (this.newSlabForm.invalid) {
return;
}
this.slabs.push(this.newSlabForm.getRawValue());
this.newSlabForm.reset({
slabType: '',
slabFrom: 0,
slabTo: 0,
commissionPercent: 0
});
}
removeSlab(index: number): void {
this.slabs.splice(index, 1);
}
exit(): void {
this.router.navigate(['/organization']);
}
save(): void {
const newSlabs = this.slabs.filter((slab) => !slab.id);
if (newSlabs.length === 0) {
return;
}
const payload = newSlabs.map((slab) => ({
slabType: CommissionSlabMasterComponent.SLAB_TYPE_CODES[slab.slabType] || slab.slabType,
slabFrom: slab.slabFrom,
slabTo: slab.slabTo,
commissionPercent: slab.commissionPercent
}));
this.organizationService.createCommissionSlabs(payload).subscribe(() => this.loadSlabs());
}
}
@@ -0,0 +1,133 @@
<!--
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="bp-container">
<div class="bp-header">
<button mat-stroked-button [routerLink]="['../']" class="bp-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
</button>
<h2 class="bp-header__title">DDS Interest &amp; Commission Batch Processing</h2>
</div>
<div class="bp-card">
<form [formGroup]="batchForm">
<div class="bp-grid">
<mat-form-field appearance="outline" class="bp-field bp-field--wide">
<mat-label>Job Type</mat-label>
<mat-select required formControlName="jobType">
@for (type of jobTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Branch</mat-label>
<mat-select required formControlName="branchScope">
@for (scope of branchScopes; track scope) {
<mat-option [value]="scope">{{ scope }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (isBranchSelected) {
<div class="bp-field bp-field--wide bp-branch-checklist">
@for (office of offices; track office.id) {
<mat-checkbox [checked]="selectedOfficeNames.has(office.name)" (change)="toggleOffice(office.name)">
{{ office.name }}
</mat-checkbox>
}
@if (offices.length === 0) {
<span class="bp-empty">Loading branches...</span>
}
</div>
}
<mat-form-field appearance="outline" class="bp-field">
<mat-label>{{ isPostingJob ? 'Posting Date' : 'As-On Date' }}</mat-label>
<input matInput required [matDatepicker]="jobDatePicker" formControlName="jobDate" />
<mat-datepicker-toggle matIconSuffix [for]="jobDatePicker"></mat-datepicker-toggle>
<mat-datepicker #jobDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Agent From</mat-label>
<input matInput required type="number" formControlName="agentFrom" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Agent To</mat-label>
<input matInput required type="number" formControlName="agentTo" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Party From</mat-label>
<input matInput required type="number" formControlName="partyFrom" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Party To</mat-label>
<input matInput required type="number" formControlName="partyTo" />
</mat-form-field>
</div>
<div class="bp-callout">
<i class="fas fa-info-circle"></i>
<span>
<strong>Generate</strong> runs a preview only — nothing is applied to any account. <strong>Post</strong> is a
separate, explicit step that commits the previewed amounts.
</span>
</div>
@if (hasGenerated) {
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-table"></i>
<h3>Preview</h3>
</div>
@if (previewRows.length === 0) {
<p class="bp-empty">
No calculation preview available yet — this appears once the backend batch-job endpoint for "{{
batchForm.get('jobType').value
}}" is connected.
</p>
} @else {
<table class="bp-table">
<thead>
<tr>
<th>Agent</th>
<th>Party</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
@for (row of previewRows; track row) {
<tr>
<td>{{ row.agent }}</td>
<td>{{ row.party }}</td>
<td>{{ row.amount | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
}
</div>
}
<div class="bp-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-stroked-button type="button" [disabled]="batchForm.invalid" (click)="generate()">
<i class="fas fa-play" style="margin-right: 6px"></i> Generate
</button>
<button mat-raised-button color="primary" type="button" [disabled]="!hasGenerated" (click)="post()">
<i class="fas fa-check" style="margin-right: 6px"></i> Post
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,145 @@
/**
* 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/.
*/
$primary: #0f766e;
.bp-container {
max-width: 900px;
margin: 0 auto;
padding: 24px 16px;
}
.bp-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.bp-back {
color: $primary;
}
.bp-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 20px 24px 24px;
}
.bp-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.bp-field {
width: 100%;
&--wide {
grid-column: span 3;
}
}
.bp-branch-checklist {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
align-items: center;
padding: 8px 0;
}
.bp-callout {
display: flex;
gap: 10px;
align-items: flex-start;
background: rgb(15 118 110 / 6%);
border-left: 3px solid $primary;
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 16px 0;
font-size: 13px;
i {
color: $primary;
margin-top: 2px;
}
}
.bp-section {
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
i {
color: $primary;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
}
}
.bp-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0;
}
.bp-table {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgb(0 0 0 / 55%);
padding: 8px 10px;
border-bottom: 2px solid rgb(0 0 0 / 10%);
}
td {
padding: 8px 10px;
border-bottom: 1px solid rgb(0 0 0 / 6%);
}
}
.bp-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
@media (width <= 900px) {
.bp-grid {
grid-template-columns: repeat(2, 1fr);
}
.bp-field--wide {
grid-column: span 2;
}
}
@@ -0,0 +1,186 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/**
* DDS Interest & Commission Batch Processing component.
*
* Six batch job types, each following the same deliberate two-step
* pattern from the legacy system: Calculate first (a dry-run preview),
* then Post as a separate, explicit confirmation — so a wrong calculation
* is never applied by accident. Modelled on the legacy Interest
* Calculation screen this replaces.
*
* Backed by the DDS Batch Run API (`/dds/batchruns`). Only the three
* "Calculation" job types can actually be generated - the three "Posting"
* entries exist in the dropdown for parity with the legacy menu, but Post
* always applies whatever run was most recently generated in this session,
* regardless of which dropdown entry is currently selected (matching how
* the Post button here is only ever gated on hasGenerated, not on jobType).
*/
@Component({
selector: 'mifosx-dds-batch-processing',
templateUrl: './dds-batch-processing.component.html',
styleUrls: ['./dds-batch-processing.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class DdsBatchProcessingComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private static readonly JOB_TYPE_CODES: Record<string, string> = {
'Interest Calculation': 'INTEREST_CALCULATION',
'OD Interest Calculation': 'OD_INTEREST_CALCULATION',
'Commission Calculation': 'COMMISSION_CALCULATION'
};
private currentBatchRunId: number | null = null;
offices: any[] = [];
selectedOfficeNames = new Set<string>();
jobTypes = [
'Interest Calculation',
'Interest Posting',
'OD Interest Calculation',
'OD Interest Posting',
'Commission Calculation',
'Commission Posting'
];
branchScopes = [
'All',
'Selected'
];
hasGenerated = false;
previewRows: Array<{ agent: string; party: string; amount: number }> = [];
batchForm: UntypedFormGroup = this.formBuilder.group({
jobType: [
'Interest Calculation',
Validators.required
],
branchScope: [
'All',
Validators.required
],
jobDate: [
new Date(),
Validators.required
],
agentFrom: [
1,
Validators.required
],
agentTo: [
99999,
Validators.required
],
partyFrom: [
1,
Validators.required
],
partyTo: [
99999999,
Validators.required
]
});
get isPostingJob(): boolean {
return (this.batchForm.get('jobType').value as string).includes('Posting');
}
get isBranchSelected(): boolean {
return this.batchForm.get('branchScope')?.value === 'Selected';
}
ngOnInit(): void {
this.organizationService.getOffices().subscribe((offices: any[]) => {
this.offices = offices || [];
});
}
toggleOffice(officeName: string): void {
if (this.selectedOfficeNames.has(officeName)) {
this.selectedOfficeNames.delete(officeName);
} else {
this.selectedOfficeNames.add(officeName);
}
}
generate(): void {
const raw = this.batchForm.getRawValue();
const jobType = DdsBatchProcessingComponent.JOB_TYPE_CODES[raw.jobType];
if (!jobType) {
// A "Posting" entry was selected - there is nothing to calculate for
// it, only to post (see class-level note on how Post is wired).
this.hasGenerated = true;
this.previewRows = [];
return;
}
const dateFormat = this.settingsService.dateFormat;
const payload: any = {
jobType,
branchScope: raw.branchScope,
jobDate: this.dateUtils.formatDate(raw.jobDate, dateFormat),
agentFrom: raw.agentFrom,
agentTo: raw.agentTo,
partyFrom: raw.partyFrom,
partyTo: raw.partyTo,
dateFormat,
locale: this.settingsService.language.code
};
if (raw.branchScope === 'Selected') {
payload.officeNames = Array.from(this.selectedOfficeNames);
}
this.organizationService.generateDdsBatchRun(payload).subscribe((result: any) => {
this.currentBatchRunId = result.resourceId;
this.organizationService.getDdsBatchRun(this.currentBatchRunId).subscribe((batchRun: any) => {
this.previewRows = (batchRun.rows || []).map((row: any) => ({
agent: row.agentCode,
party: row.partyCode || '—',
amount: row.amount
}));
this.hasGenerated = true;
});
});
}
post(): void {
if (!this.currentBatchRunId) {
return;
}
this.organizationService.postDdsBatchRun(this.currentBatchRunId).subscribe(() => {
this.currentBatchRunId = null;
this.hasGenerated = false;
this.previewRows = [];
});
}
exit(): void {
this.router.navigate(['/organization']);
}
}
@@ -17,6 +17,9 @@ import { Route } from '../core/route/route.service';
import { OrganizationComponent } from './organization.component'; import { OrganizationComponent } from './organization.component';
import { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.component'; import { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.component';
import { OfficesComponent } from './offices/offices.component'; import { OfficesComponent } from './offices/offices.component';
import { AgentMasterComponent } from './agent-master/agent-master.component';
import { CommissionSlabMasterComponent } from './commission-slab-master/commission-slab-master.component';
import { DdsBatchProcessingComponent } from './dds-batch-processing/dds-batch-processing.component';
import { EmployeesComponent } from './employees/employees.component'; import { EmployeesComponent } from './employees/employees.component';
import { CreateEmployeeComponent } from './employees/create-employee/create-employee.component'; import { CreateEmployeeComponent } from './employees/create-employee/create-employee.component';
import { ViewEmployeeComponent } from './employees/view-employee/view-employee.component'; import { ViewEmployeeComponent } from './employees/view-employee/view-employee.component';
@@ -237,6 +240,21 @@ const routes: Routes = [
} }
] ]
}, },
{
path: 'agent-master',
component: AgentMasterComponent,
data: { title: 'Agent Master', breadcrumb: 'Agent Master' }
},
{
path: 'commission-slab-master',
component: CommissionSlabMasterComponent,
data: { title: 'Commission Slab Master', breadcrumb: 'Commission Slab Master' }
},
{
path: 'dds-batch-processing',
component: DdsBatchProcessingComponent,
data: { title: 'DDS Interest & Commission Batch', breadcrumb: 'DDS Batch Processing' }
},
{ {
path: 'employees', path: 'employees',
data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' }, data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' },
@@ -90,6 +90,47 @@
</a> </a>
</div> </div>
<div #agentMaster>
<a class="org-card" [routerLink]="['agent-master']">
<div class="org-card__icon org-card__icon--teal">
<i class="fas fa-user-tie"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Agent Master</h3>
<p class="org-card__desc">
Register Daily Deposit Scheme collection agents, their commission/TDS settings, limits and guarantors
</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<div #commissionSlabMaster>
<a class="org-card" [routerLink]="['commission-slab-master']">
<div class="org-card__icon org-card__icon--teal">
<i class="fas fa-percentage"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Commission Slab Master</h3>
<p class="org-card__desc">Define collection-volume and override commission percentage slabs</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<div #ddsBatchProcessing>
<a class="org-card" [routerLink]="['dds-batch-processing']">
<div class="org-card__icon org-card__icon--teal">
<i class="fas fa-cogs"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">DDS Interest &amp; Commission Batch</h3>
<p class="org-card__desc">Calculate then post interest, OD interest, and commission for DDS accounts</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<!-- Manage Loan Originators --> <!-- Manage Loan Originators -->
<div #manageLoanOriginators> <div #manageLoanOriginators>
<a class="org-card" [routerLink]="['manage-loan-originators']" *mifosxHasPermission="'READ_LOAN_ORIGINATOR'"> <a class="org-card" [routerLink]="['manage-loan-originators']" *mifosxHasPermission="'READ_LOAN_ORIGINATOR'">
@@ -233,6 +233,141 @@ export class OrganizationService {
return this.http.get('/currencies'); return this.http.get('/currencies');
} }
/**
* @returns {Observable<any>} DDS Agents data
*/
getAgents(): Observable<any> {
return this.http.get('/dds/agents');
}
/**
* @param {string | number} agentId Agent ID.
* @returns {Observable<any>} DDS Agent.
*/
getAgent(agentId: string | number): Observable<any> {
return this.http.get(`/dds/agents/${agentId}`);
}
/**
* @param {any} agent Agent to be created.
* @returns {Observable<any>}
*/
createAgent(agent: any): Observable<any> {
return this.http.post('/dds/agents', agent);
}
/**
* @param {string | number} agentId Agent ID.
* @param {any} agent Agent changes to apply.
* @returns {Observable<any>}
*/
updateAgent(agentId: string | number, agent: any): Observable<any> {
return this.http.put(`/dds/agents/${agentId}`, agent);
}
/**
* @returns {Observable<any>} DDS Parties data
*/
getParties(): Observable<any> {
return this.http.get('/dds/parties');
}
/**
* @param {string | number} partyId Party ID.
* @returns {Observable<any>} DDS Party.
*/
getParty(partyId: string | number): Observable<any> {
return this.http.get(`/dds/parties/${partyId}`);
}
/**
* @param {any} party Party to be created.
* @returns {Observable<any>}
*/
createParty(party: any): Observable<any> {
return this.http.post('/dds/parties', party);
}
/**
* @param {string | number} partyId Party ID.
* @param {any} party Party changes to apply.
* @returns {Observable<any>}
*/
updateParty(partyId: string | number, party: any): Observable<any> {
return this.http.put(`/dds/parties/${partyId}`, party);
}
/**
* @param {string | number} partyId Party ID.
* @param {any} closeCommand { closeDate }
* @returns {Observable<any>}
*/
closeParty(partyId: string | number, closeCommand: any): Observable<any> {
return this.http.post(`/dds/parties/${partyId}?command=close`, closeCommand);
}
/**
* @param {any} transaction Daily transaction batch (PARTY_TRAN collections or a WITHDRAW_DEPOSITE transfer) to post.
* @returns {Observable<any>}
*/
createPartyTransaction(transaction: any): Observable<any> {
return this.http.post('/dds/partytransactions', transaction);
}
/**
* @returns {Observable<any>} DDS Commission Slabs data
*/
getCommissionSlabs(): Observable<any> {
return this.http.get('/dds/commissionslabs');
}
/**
* @param {any[]} slabs New commission slab rows to add to the chart.
* @returns {Observable<any>}
*/
createCommissionSlabs(slabs: any[]): Observable<any> {
return this.http.post('/dds/commissionslabs', { slabs });
}
/**
* @param {any} batch Batch run filters (jobType, branchScope, jobDate, agentFrom/To, partyFrom/To) to generate a preview for.
* @returns {Observable<any>}
*/
generateDdsBatchRun(batch: any): Observable<any> {
return this.http.post('/dds/batchruns', batch);
}
/**
* @param {string | number} batchRunId Batch run ID.
* @returns {Observable<any>} Batch run, including its previewed/posted rows.
*/
getDdsBatchRun(batchRunId: string | number): Observable<any> {
return this.http.get(`/dds/batchruns/${batchRunId}`);
}
/**
* @param {string | number} batchRunId Batch run ID to post - applies exactly the rows it previewed.
* @returns {Observable<any>}
*/
postDdsBatchRun(batchRunId: string | number): Observable<any> {
return this.http.post(`/dds/batchruns/${batchRunId}`, {});
}
/**
* @param {string} reportKey One of the backend DDS report keys.
* @param {any} filters fromDate, toDate (ISO strings), agentFrom/To, partyFrom/To.
* @returns {Observable<any>} { reportKey, available, message, columns, rows }
*/
runDdsReport(reportKey: string, filters: any): Observable<any> {
let httpParams = new HttpParams();
Object.keys(filters || {}).forEach((key) => {
if (filters[key] !== undefined && filters[key] !== null && filters[key] !== '') {
httpParams = httpParams.set(key, filters[key]);
}
});
return this.http.get(`/dds/reports/${reportKey}`, { params: httpParams });
}
/** /**
* @param {any[]} currencies * @param {any[]} currencies
* @returns {Observable<any>} Currencies data * @returns {Observable<any>} Currencies data
@@ -0,0 +1,131 @@
<!--
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="rdp-container">
<div class="rdp-header">
<i class="fas fa-calendar-day rdp-header__icon"></i>
<h2 class="rdp-header__title">{{ 'labels.heading.Create Daily Deposit Scheme Product' | translate }}</h2>
</div>
<!-- Currency/Terms/Settings/Charges/Accounting still run underneath (the account-creation
engine needs them) but are pre-filled with fixed defaults in ngAfterViewInit() rather than
shown - the legacy Account Type Creation screen never asked for any of this either. Kept
mounted (just hidden) so their forms exist for the "product" payload to read from. -->
<div hidden>
<mifosx-daily-deposit-scheme-product-currency-step
[DailyDepositSchemeProductsTemplate]="productsTemplate"
></mifosx-daily-deposit-scheme-product-currency-step>
<mifosx-daily-deposit-scheme-product-terms-step
[DailyDepositSchemeProductsTemplate]="productsTemplate"
></mifosx-daily-deposit-scheme-product-terms-step>
<mifosx-daily-deposit-scheme-product-settings-step
[DailyDepositSchemeProductsTemplate]="productsTemplate"
></mifosx-daily-deposit-scheme-product-settings-step>
<mifosx-daily-deposit-scheme-product-charges-step
[DailyDepositSchemeProductsTemplate]="productsTemplate"
[currencyCode]="currencyForm.get('currencyCode')"
></mifosx-daily-deposit-scheme-product-charges-step>
<mifosx-daily-deposit-scheme-product-accounting-step
[DailyDepositSchemeProductsTemplate]="productsTemplate"
[accountingRuleData]="accountingRuleData"
[DailyDepositSchemeProductFormValid]="productFormValid"
[isDormancyTrackingActive]="currencyForm.get('dormancyAlert')"
></mifosx-daily-deposit-scheme-product-accounting-step>
</div>
<mat-tab-group
class="rdp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-info-circle rdp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-details-step
[DailyDepositSchemeProductsTemplate]="productsTemplate"
></mifosx-daily-deposit-scheme-product-details-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-chart-area rdp-tab-icon"></i>
CBS Base Chart
</ng-template>
<div class="rdp-tab-content">
@if (activeBaseVersion) {
<div class="base-rate-card">
<div class="base-rate-header">
<div>
<h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
<p class="base-rate-meta">
Active CBS base chart
<span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</p>
</div>
<span class="base-rate-badge">Active</span>
</div>
@if (activeBaseVersion.remarks) {
<p class="base-rate-remarks">{{ activeBaseVersion.remarks }}</p>
}
<table class="base-rate-table" mat-table [dataSource]="activeBaseVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? 'N/A' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="rdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: rdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div>
</mat-tab>
</mat-tab-group>
<div class="rdp-action-bar">
<button mat-raised-button [routerLink]="['../']" class="rdp-action-bar__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<div class="rdp-action-bar__right">
<button mat-raised-button color="primary" [disabled]="!productFormValid" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }}
</button>
</div>
</div>
</div>
@@ -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/.
*/
$primary: #0f766e;
.rdp-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.rdp-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
&__icon {
font-size: 20px;
color: $primary;
}
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.rdp-tabs {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
::ng-deep {
.mat-mdc-tab-header {
border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.mat-mdc-tab {
min-width: 100px;
padding: 0 12px;
height: 52px;
}
.mat-mdc-tab-body-wrapper {
min-height: 200px;
}
.mat-mdc-tab-body {
overflow: visible !important;
}
.mat-mdc-tab-body-content {
overflow: visible !important;
}
}
}
.rdp-tab-icon {
margin-right: 8px;
font-size: 14px;
}
.rdp-tab-content {
padding: 24px 16px;
}
.base-rate-card {
padding: 16px;
border: 1px solid #dbe4f0;
border-radius: 14px;
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.base-rate-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.base-rate-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.base-rate-meta {
margin: 6px 0 0;
color: #5b6b7f;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.base-rate-badge {
padding: 4px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.base-rate-remarks {
margin: 0 0 12px;
color: #334155;
}
.base-rate-table {
width: 100%;
border-collapse: collapse;
}
.rdp-action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel {
color: rgb(0 0 0 / 60%);
}
&__right {
display: flex;
gap: 12px;
}
}
@@ -0,0 +1,346 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { AfterViewInit, Component, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
/** Custom Components — Daily Deposit Scheme's own stepper, kept independent
* of Recurring Deposit Products so future changes to either never cross over. */
import { DailyDepositSchemeProductDetailsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component';
import { DailyDepositSchemeProductCurrencyStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component';
import { DailyDepositSchemeProductTermsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component';
import { DailyDepositSchemeProductSettingsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component';
import { DailyDepositSchemeProductChargesStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component';
import { DailyDepositSchemeProductAccountingStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { Dates } from 'app/core/utils/dates';
import { MatTabsModule } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-create-daily-deposit-scheme-product',
templateUrl: './create-daily-deposit-scheme-product.component.html',
styleUrls: ['./create-daily-deposit-scheme-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTabsModule,
DailyDepositSchemeProductDetailsStepComponent,
DailyDepositSchemeProductCurrencyStepComponent,
DailyDepositSchemeProductTermsStepComponent,
DailyDepositSchemeProductSettingsStepComponent,
DailyDepositSchemeProductChargesStepComponent,
DailyDepositSchemeProductAccountingStepComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
]
})
export class CreateDailyDepositSchemeProductComponent implements AfterViewInit {
private route = inject(ActivatedRoute);
private productsService = inject(ProductsService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(DailyDepositSchemeProductDetailsStepComponent, { static: true })
detailsStep: DailyDepositSchemeProductDetailsStepComponent;
@ViewChild(DailyDepositSchemeProductCurrencyStepComponent, { static: true })
currencyStep: DailyDepositSchemeProductCurrencyStepComponent;
@ViewChild(DailyDepositSchemeProductTermsStepComponent, { static: true })
termsStep: DailyDepositSchemeProductTermsStepComponent;
@ViewChild(DailyDepositSchemeProductSettingsStepComponent, { static: true })
settingsStep: DailyDepositSchemeProductSettingsStepComponent;
@ViewChild(DailyDepositSchemeProductChargesStepComponent, { static: true })
chargesStep: DailyDepositSchemeProductChargesStepComponent;
@ViewChild(DailyDepositSchemeProductAccountingStepComponent, { static: true })
accountingStep: DailyDepositSchemeProductAccountingStepComponent;
productsTemplate: any;
accountingRuleData: string[] = [];
selectedTabIndex = 0;
rdInterestRates: any[] = [];
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
constructor() {
this.route.data.subscribe((data: { dailyDepositSchemeProductsTemplate: any }) => {
this.productsTemplate = data.dailyDepositSchemeProductsTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
}
ngAfterViewInit(): void {
// The DDS product screen only shows the legacy Account Type Creation fields (Details tab) -
// currency/terms/settings/charges/accounting still exist underneath (the account-creation
// engine needs them), but they're filled in here with fixed sensible defaults rather than
// asked of the user, since the legacy screen never asked for them either.
const template = this.productsTemplate || {};
this.currencyForm.patchValue({
currencyCode: this.currencyForm.value.currencyCode || 'INR',
digitsAfterDecimal: this.currencyForm.value.digitsAfterDecimal ?? 2
});
const firstOptionId = (options: any[]) => options?.[0]?.id ?? '';
this.termsForm.patchValue({
interestCompoundingPeriodType: firstOptionId(template.interestCompoundingPeriodTypeOptions),
interestCalculationDaysInYearType: firstOptionId(template.interestCalculationDaysInYearTypeOptions),
minDepositAmount: this.termsForm.value.minDepositAmount || 1,
maxDepositAmount: this.termsForm.value.maxDepositAmount || 999999999
});
// accountingRule 1 = None, matching the legacy screen (which never asked for GL mappings) -
// this also clears every accounting-tab required-field validator, since those only apply
// under Cash/Accrual accounting.
this.accountingForm.patchValue({ accountingRule: 1 });
this.detailsForm.get('autoInterestPost')?.valueChanges.subscribe(() => this.syncAutoInterestPost());
this.detailsForm.get('autoInterestPostWhen')?.valueChanges.subscribe(() => this.syncAutoInterestPost());
this.syncAutoInterestPost();
}
/** Mirrors the simple "Auto Interest Post" / "Auto Interest Post When" / "Auto Interest Post
* Day" fields onto the (hidden) terms-step fields that the account-creation engine actually
* reads. */
private syncAutoInterestPost(): void {
const autoPost = this.detailsForm?.value.autoInterestPost ?? true;
const postingPeriodType = this.detailsForm?.value.autoInterestPostWhen;
const postingDay = this.detailsForm?.value.autoInterestPostDay;
this.termsForm?.patchValue({
autoPost,
interestPostingPeriodType: postingPeriodType || this.termsForm.value.interestPostingPeriodType,
frequencyFixDay: postingDay || this.termsForm.value.frequencyFixDay
});
}
get detailsForm() {
return this.detailsStep?.DailyDepositSchemeProductDetailsForm;
}
get currencyForm() {
return this.currencyStep?.DailyDepositSchemeProductCurrencyForm;
}
get termsForm() {
return this.termsStep?.DailyDepositSchemeProductTermsForm;
}
get settingsForm() {
return this.settingsStep?.DailyDepositSchemeProductSettingsForm;
}
get accountingForm() {
return this.accountingStep?.DailyDepositSchemeProductAccountingForm;
}
get productFormValid() {
return !!this.detailsForm?.valid;
}
get activeBaseVersion() {
const chart =
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE' && version.isActive) ??
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE') ??
this.rdInterestRates[0];
if (chart) {
return {
...chart,
versionLabel: chart.versionLabel?.replace('FD', 'DDS'),
remarks: chart.remarks?.replace('FD', 'DDS')
};
}
return null;
}
get activeBaseSlabs() {
// The base chart is borrowed from an FD product purely for its period/amount-range
// *structure* - the account-creation engine needs some chart to exist. Its actual rate is
// never used: every slab's annualInterestRate is overridden with the Details tab's own
// Interest Rate field, so DDS never depends on FD's real interest figures.
const rate = this.detailsForm?.value.interestRate;
return (this.activeBaseVersion?.slabs || []).map((slab: any) => ({
...slab,
annualInterestRate: rate ?? slab.annualInterestRate
}));
}
get product() {
// accountTypeGroup / autoInterestPost / autoInterestPostWhen / autoInterestPostDay are the
// legacy Account Type Creation screen's own fields - not real product API parameters. The
// auto-interest-post trio is already mirrored onto termsStep (autoPost /
// interestPostingPeriodType / frequencyFixDay) via syncAutoInterestPost(); accountTypeGroup
// is folded into the description here instead.
const {
accountTypeGroup,
autoInterestPost,
autoInterestPostWhen,
autoInterestPostDay,
interestRate,
...detailsPayload
} = this.detailsStep.DailyDepositSchemeProductDetails;
const description = [
accountTypeGroup,
detailsPayload.description
]
.filter((part) => !!part)
.join(' - ');
return {
...detailsPayload,
description,
...this.currencyStep.DailyDepositSchemeProductCurrency,
...this.termsStep.DailyDepositSchemeProductTerms,
...this.settingsStep.DailyDepositSchemeProductSettings,
...this.chargesStep.DailyDepositSchemeProductCharges,
...this.accountingStep.DailyDepositSchemeProductAccounting,
charts: this.productCharts,
rdSlabs: this.activeBaseSlabs
};
}
get productCharts() {
const activeBaseVersion = this.activeBaseVersion;
if (!activeBaseVersion?.slabs?.length) {
return [];
}
const dateFormat = this.settingsService.dateFormat;
return [
{
name: activeBaseVersion.versionLabel || 'CBS Base Chart',
description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart',
fromDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) ||
activeBaseVersion.effectiveFromDate,
...(activeBaseVersion.effectiveToDate
? {
endDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) ||
activeBaseVersion.effectiveToDate
}
: {}),
isPrimaryGroupingByAmount: false,
locale: this.settingsService.language.code,
dateFormat,
chartSlabs: this.activeBaseSlabs.map((slab: any) => {
const chartSlab: any = {
periodType: slab.periodTypeEnum,
fromPeriod: slab.fromPeriod,
toPeriod: slab.toPeriod,
annualInterestRate: slab.annualInterestRate,
description: slab.description || '',
incentives: [] as any[]
};
if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') {
chartSlab.amountRangeFrom = slab.amountRangeFrom;
}
if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') {
chartSlab.amountRangeTo = slab.amountRangeTo;
}
return chartSlab;
})
}
] as any[];
}
submit() {
const product: any = this.pruneEmptyValues({
...this.product,
charts: this.productCharts,
charges: (this.product.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code,
productCategory: 'DDS'
});
if (!product.description) {
product.description = '';
}
delete product.advancedAccountingRules;
this.productsService.createRecurringDepositProduct(product).subscribe((response: any) => {
this.router.navigate(
[
'../',
response.resourceId
],
{ relativeTo: this.route }
);
});
}
private pruneEmptyValues<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T;
}
if (value && typeof value === 'object') {
return Object.entries(value as Record<string, any>).reduce(
(acc, [
key,
currentValue
]) => {
const cleanedValue = this.pruneEmptyValues(currentValue);
if (cleanedValue === undefined) {
return acc;
}
return {
...acc,
[key]: cleanedValue
};
},
{} as Record<string, any>
) as T;
}
if (value === '' || value === null || value === undefined) {
return undefined as T;
}
return value;
}
}
@@ -0,0 +1,35 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../../products.service';
/**
* Daily Deposit Scheme Products template resolver.
*
* Reuses the Recurring Deposit product template — a Daily Deposit Scheme
* product is created through the same underlying product-creation engine.
*/
@Injectable()
export class DailyDepositSchemeProductsTemplateResolver {
private productsService = inject(ProductsService);
/**
* Returns the Daily Deposit Scheme products template.
* @returns {Observable<any>}
*/
resolve(): Observable<any> {
return this.productsService.getRecurringDepositProductsTemplate();
}
}
@@ -0,0 +1,121 @@
<!--
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/.
-->
<form [formGroup]="DailyDepositSchemeProductAccountingForm">
<div class="rdpa-section">
<div class="rdpa-section__header">
<span class="rdpa-section__icon">
<i class="fas fa-book"></i>
</span>
<h3>Accounting</h3>
</div>
<div class="rdpa-section__body">
<div class="rdpa-group">
<div class="rdpa-group__header">
<h4>Accounting Rule</h4>
</div>
<div class="rdpa-radio-group">
<mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
</div>
</div>
@if (isCashOrAccrualAccounting()) {
@for (section of accountingSectionConfigs; track section.key) {
<div class="rdpa-group">
<div class="rdpa-group__header">
<h4>{{ section.title }}</h4>
<button type="button" class="rdpa-add-btn" (click)="addOtherGlMapping(section.otherMappingsControlName)">
+ Add Other
</button>
</div>
<div class="rdpa-list">
@for (row of section.rows; track row.key) {
@if (
(!row.accrualOnly || isAccrualAccounting()) && (!row.dormancyOnly || isDormancyTrackingEnabled())
) {
<div class="rdpa-row">
<div class="rdpa-row__meta">
<span
class="rdpa-pill"
[class.rdpa-pill--green]="section.pillClass === 'green'"
[class.rdpa-pill--amber]="section.pillClass === 'amber'"
[class.rdpa-pill--blue]="section.pillClass === 'blue'"
>
{{ section.pillLabel }}
</span>
<span class="rdpa-row__label">{{ row.label }}</span>
</div>
<mifosx-gl-account-selector
class="rdpa-row__input"
[inputFormControl]="DailyDepositSchemeProductAccountingForm.get(row.controlName)"
[glAccountList]="row.options"
[required]="row.required"
[inputLabel]="row.inputLabel"
></mifosx-gl-account-selector>
</div>
}
}
</div>
<div class="rdpa-list" [formArrayName]="section.otherMappingsControlName">
@for (
otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
track $index;
let i = $index
) {
<div class="rdpa-other-row-wrap">
<div class="rdpa-row rdpa-row--other" [formGroupName]="i">
<div class="rdpa-row__meta rdpa-row__meta--other">
<span
class="rdpa-pill"
[class.rdpa-pill--green]="section.pillClass === 'green'"
[class.rdpa-pill--amber]="section.pillClass === 'amber'"
[class.rdpa-pill--blue]="section.pillClass === 'blue'"
>
{{ section.pillLabel }}
</span>
<input
class="rdpa-row__name rdpa-row__name--other-account"
formControlName="accountName"
placeholder="Enter account name..."
type="text"
/>
</div>
<div class="rdpa-row__actions rdpa-row__actions--other">
<mat-form-field class="rdpa-row__other-field" appearance="outline" subscriptSizing="dynamic">
<mat-label>GL Code</mat-label>
<mat-select
[formControl]="getOtherGlMappingControl(section.otherMappingsControlName, i, 'glAccountId')"
>
@for (glAccount of section.otherAccountOptions; track glAccount.id) {
<mat-option [value]="glAccount.id">
{{ '(' + glAccount.glCode + ') ' + glAccount.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<button
type="button"
class="rdpa-remove-btn rdpa-remove-btn--side"
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
>
<i class="fas fa-times"></i>
</button>
</div>
}
</div>
</div>
}
}
</div>
</div>
</form>
@@ -0,0 +1,303 @@
/**
* 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/.
*/
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-700: #334155;
$gray-900: #202124;
.rdpa-section {
border: 1px solid $border;
border-radius: 10px;
background: #fff;
}
.rdpa-section__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
.rdpa-section__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
color: #2563eb;
font-size: 13px;
}
.rdpa-section__body {
padding: 16px 24px 20px;
}
.rdpa-group {
& + & {
margin-top: 20px;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin: 0 0 12px;
}
h4 {
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #94a3b8;
}
}
.rdpa-add-btn {
border: 0;
background: transparent;
color: #2563eb;
font-size: 12px;
font-weight: 700;
padding: 0;
cursor: pointer;
}
.rdpa-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.rdpa-list + .rdpa-list {
margin-top: 12px;
}
.rdpa-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 12px;
border: 1px solid $border;
border-radius: 8px;
background: #fff;
@media (width <= 900px) {
flex-direction: column;
align-items: stretch;
}
}
.rdpa-row:has(mifosx-gl-account-selector) {
align-items: stretch;
}
.rdpa-row--other {
border-style: dashed;
border-color: #cbd5e1;
background: #f8fafc;
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
align-items: center;
padding: 12px 16px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.rdpa-other-row-wrap {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
gap: 8px;
}
}
.rdpa-row__meta {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1 1 auto;
}
.rdpa-row__meta--other {
min-width: 0;
flex: 1 1 auto;
align-self: stretch;
}
.rdpa-row__label {
font-size: 14px;
font-weight: 600;
color: $gray-700;
}
.rdpa-row__input {
flex: 0 0 22rem;
max-width: 22rem;
min-width: 18rem;
@media (width <= 900px) {
width: 100%;
max-width: 100%;
min-width: 0;
}
}
.rdpa-row__input :is(mifosx-gl-account-selector) {
width: 100%;
display: block;
}
.rdpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) {
width: 100%;
margin-right: 0 !important;
}
.rdpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) {
width: 100%;
}
.rdpa-row__actions {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.rdpa-row__actions--other {
justify-content: flex-end;
align-self: stretch;
width: 100%;
@media (width <= 1100px) {
justify-content: flex-start;
}
}
.rdpa-row__name {
width: 100%;
min-width: 0;
max-width: 24rem;
height: 40px;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
background: #fff;
@media (width <= 900px) {
max-width: 100%;
}
}
.rdpa-row__name--other-account {
max-width: 100%;
flex: 1 1 auto;
height: 48px;
}
.rdpa-row__other-field {
width: 100%;
min-width: 0;
}
.rdpa-row__other-field :is(mat-form-field) {
width: 100%;
}
.rdpa-row__other-field :is(.mat-mdc-form-field-required-marker) {
display: none;
}
.rdpa-remove-btn {
border: 0;
background: transparent;
color: #ef4444;
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
.rdpa-remove-btn--side {
place-self: center end;
margin-right: 4px;
@media (width <= 1100px) {
justify-self: start;
margin-right: 0;
}
}
.rdpa-radio-group {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.rdpa-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
&--green {
background: #dcfce7;
color: #16a34a;
}
&--amber {
background: #fef3c7;
color: #d97706;
}
&--blue {
background: #dbeafe;
color: #2563eb;
}
}
@media (width <= 900px) {
.rdpa-row__meta {
margin-bottom: 4px;
}
.rdpa-row__input {
flex: 1 1 auto;
max-width: 100%;
}
}
@@ -0,0 +1,423 @@
/**
* 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, inject } from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component';
import { Accounting } from 'app/core/utils/accounting';
import { GLAccount } from 'app/shared/models/general.model';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income';
type AccountingRowConfig = {
key: string;
label: string;
inputLabel: string;
controlName: string;
options: GLAccount[];
required?: boolean;
accrualOnly?: boolean;
dormancyOnly?: boolean;
};
type AccountingSectionConfig = {
key: AccountingSectionKey;
title: string;
pillLabel: string;
pillClass: 'green' | 'amber' | 'blue';
rows: AccountingRowConfig[];
otherMappingsControlName: string;
otherAccountOptions: GLAccount[];
};
type OtherGlMappingValue = {
accountName: string;
glAccountId: number | null;
};
@Component({
selector: 'mifosx-daily-deposit-scheme-product-accounting-step',
templateUrl: './daily-deposit-scheme-product-accounting-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-accounting-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
ReactiveFormsModule,
MatRadioGroup,
MatRadioButton,
GlAccountSelectorComponent
]
})
export class DailyDepositSchemeProductAccountingStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private accounting = inject(Accounting);
@Input() DailyDepositSchemeProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() DailyDepositSchemeProductFormValid: boolean;
@Input() isDormancyTrackingActive: any;
DailyDepositSchemeProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = [];
liabilityAccountOptions: GLAccount[] = [];
expenseAccountOptions: GLAccount[] = [];
incomeAccountOptions: GLAccount[] = [];
accountingSectionConfigs: AccountingSectionConfig[] = [];
constructor() {
this.createDailyDepositSchemeProductAccountingForm();
this.setConditionalControls();
}
ngOnInit() {
const accountingMappings = this.DailyDepositSchemeProductsTemplate?.accountingMappings || {};
const mappings = {
...accountingMappings,
otherGlMappingsMap: this.DailyDepositSchemeProductsTemplate?.otherGlMappingsMap
};
const accountingMappingOptions = this.DailyDepositSchemeProductsTemplate?.accountingMappingOptions || {};
// Defaults to Cash (2) for a brand-new product (no existing accountingRule yet), but preserves
// the product's own current accounting rule when editing - previously this was hardcoded to 2
// unconditionally, which silently forced every edited product onto Cash-based accounting (and
// its required GL mappings) even for products that were and should remain "None".
const accountingRuleId = this.DailyDepositSchemeProductsTemplate?.accountingRule?.id ?? 2;
this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
this.buildAccountingSectionConfigs();
this.DailyDepositSchemeProductAccountingForm.patchValue({ accountingRule: accountingRuleId });
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
this.setDormancyControl();
}
createDailyDepositSchemeProductAccountingForm() {
this.DailyDepositSchemeProductAccountingForm = this.formBuilder.group({
accountingRule: [2],
savingsReferenceAccountId: [''],
savingsControlAccountId: [''],
transfersInSuspenseAccountId: [''],
interestOnSavingsAccountId: [''],
incomeFromFeeAccountId: [''],
incomeFromPenaltyAccountId: [''],
feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''],
escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
incomeOtherGlMappings: this.formBuilder.array([])
});
}
private setConditionalControls() {
this.DailyDepositSchemeProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => {
this.syncAccountingControls(rule);
});
}
private patchAccountingValues(mappings: any) {
this.DailyDepositSchemeProductAccountingForm.patchValue({
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
interestPayableAccountId: mappings.interestPayableAccount?.id || '',
escheatLiabilityId: mappings.escheatLiabilityAccount?.id || ''
});
if (this.isCashOrAccrualAccounting()) {
const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings;
this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
this.setOtherGlMappings(
'liabilitiesOtherGlMappings',
glMappings?.liabilities || [],
this.liabilityAccountOptions
);
this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
}
}
private createOtherGlMappingGroup(otherGlMapping: Partial<OtherGlMappingValue> = {}) {
return this.formBuilder.group({
accountName: [
otherGlMapping.accountName || '',
Validators.required
],
glAccountId: [
otherGlMapping.glAccountId || '',
Validators.required
]
});
}
getOtherGlMappingsArray(controlName: string): UntypedFormArray {
return this.DailyDepositSchemeProductAccountingForm.get(controlName) as UntypedFormArray;
}
private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) {
const arr = this.getOtherGlMappingsArray(controlName);
arr.clear();
(otherGlMappings || []).forEach((m) => {
arr.push(
this.createOtherGlMappingGroup({
accountName: m.accountName,
glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null
})
);
});
}
addOtherGlMapping(controlName: string) {
this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
}
removeOtherGlMapping(controlName: string, index: number) {
this.getOtherGlMappingsArray(controlName).removeAt(index);
}
getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl {
return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl;
}
private clearOtherGlMappings() {
[
'assetsOtherGlMappings',
'liabilitiesOtherGlMappings',
'expensesOtherGlMappings',
'incomeOtherGlMappings'
].forEach((c) => this.getOtherGlMappingsArray(c).clear());
}
private getOtherGlMappings() {
const map = (controlName: string, options: GLAccount[]) =>
this.getOtherGlMappingsArray(controlName)
.value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId)
.map((item: OtherGlMappingValue) => ({
accountName: item.accountName,
glCode: options.find((o) => o.id === item.glAccountId)?.glCode || ''
}))
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
const otherGlMappings = {
assets: map('assetsOtherGlMappings', this.assetAccountOptions),
liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
expenses: map('expensesOtherGlMappings', this.expenseAccountOptions),
income: map('incomeOtherGlMappings', this.incomeAccountOptions)
};
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
}
private setDormancyControl() {
this.isDormancyTrackingActive?.valueChanges?.subscribe(() => {
this.syncAccountingControls(this.DailyDepositSchemeProductAccountingForm.value.accountingRule);
});
}
isDormancyTrackingEnabled(): boolean {
return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false;
}
private syncAccountingControls(accountingRule: number) {
const setRequired = (name: string, required: boolean) => {
const ctrl = this.DailyDepositSchemeProductAccountingForm.get(name);
if (!ctrl) return;
if (required) {
ctrl.setValidators(Validators.required);
} else {
ctrl.clearValidators();
}
ctrl.updateValueAndValidity({ emitEvent: false });
};
const cashOrAccrual = this.accounting.isCashOrAccrualAccountingRuleId(accountingRule);
const accrualOnly = this.accounting.isAccrualAccountingRuleId(accountingRule);
setRequired('savingsReferenceAccountId', cashOrAccrual);
setRequired('savingsControlAccountId', cashOrAccrual);
setRequired('transfersInSuspenseAccountId', cashOrAccrual);
setRequired('interestOnSavingsAccountId', cashOrAccrual);
setRequired('incomeFromFeeAccountId', cashOrAccrual);
setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
setRequired('feesReceivableAccountId', accrualOnly);
setRequired('penaltiesReceivableAccountId', accrualOnly);
setRequired('interestPayableAccountId', accrualOnly);
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
if (!cashOrAccrual) {
this.clearOtherGlMappings();
}
}
buildAccountingSectionConfigs() {
this.accountingSectionConfigs = [
{
key: 'assets',
title: 'Assets',
pillLabel: 'Asset',
pillClass: 'green',
rows: [
{
key: 'savingsReferenceAccountId',
label: 'Savings Reference Account',
inputLabel: 'Savings reference account',
controlName: 'savingsReferenceAccountId',
options: this.assetAccountOptions,
required: true
},
{
key: 'feesReceivableAccountId',
label: 'Fees Receivable Account',
inputLabel: 'Fees receivable account',
controlName: 'feesReceivableAccountId',
options: this.assetAccountOptions,
accrualOnly: true
},
{
key: 'penaltiesReceivableAccountId',
label: 'Penalties Receivable Account',
inputLabel: 'Penalties receivable account',
controlName: 'penaltiesReceivableAccountId',
options: this.assetAccountOptions,
accrualOnly: true
}
],
otherMappingsControlName: 'assetsOtherGlMappings',
otherAccountOptions: this.assetAccountOptions
},
{
key: 'liabilities',
title: 'Liabilities',
pillLabel: 'Liability',
pillClass: 'green',
rows: [
{
key: 'savingsControlAccountId',
label: 'Savings Control Account',
inputLabel: 'Savings control account',
controlName: 'savingsControlAccountId',
options: this.liabilityAccountOptions,
required: true
},
{
key: 'transfersInSuspenseAccountId',
label: 'Transfers In Suspense Account',
inputLabel: 'Transfers in suspense account',
controlName: 'transfersInSuspenseAccountId',
options: this.liabilityAccountOptions,
required: true
},
{
key: 'interestPayableAccountId',
label: 'Interest Payable Account',
inputLabel: 'Interest payable account',
controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions,
accrualOnly: true
},
{
key: 'escheatLiabilityId',
label: 'Escheat Liability Account',
inputLabel: 'Escheat liability account',
controlName: 'escheatLiabilityId',
options: this.liabilityAccountOptions,
dormancyOnly: true
}
],
otherMappingsControlName: 'liabilitiesOtherGlMappings',
otherAccountOptions: this.liabilityAccountOptions
},
{
key: 'expenses',
title: 'Expenses',
pillLabel: 'Expense',
pillClass: 'amber',
rows: [
{
key: 'interestOnSavingsAccountId',
label: 'Interest On Savings Account',
inputLabel: 'Interest on savings account',
controlName: 'interestOnSavingsAccountId',
options: this.expenseAccountOptions,
required: true
}
],
otherMappingsControlName: 'expensesOtherGlMappings',
otherAccountOptions: this.expenseAccountOptions
},
{
key: 'income',
title: 'Income',
pillLabel: 'Income',
pillClass: 'blue',
rows: [
{
key: 'incomeFromFeeAccountId',
label: 'Income From Fees',
inputLabel: 'Income from fees',
controlName: 'incomeFromFeeAccountId',
options: this.incomeAccountOptions,
required: true
},
{
key: 'incomeFromPenaltyAccountId',
label: 'Income From Penalties',
inputLabel: 'Income from penalties',
controlName: 'incomeFromPenaltyAccountId',
options: this.incomeAccountOptions,
required: true
}
],
otherMappingsControlName: 'incomeOtherGlMappings',
otherAccountOptions: this.incomeAccountOptions
}
];
}
get DailyDepositSchemeProductAccounting() {
const {
assetsOtherGlMappings,
liabilitiesOtherGlMappings,
expensesOtherGlMappings,
incomeOtherGlMappings,
...accounting
} = this.DailyDepositSchemeProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings();
return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) };
}
isCashOrAccrualAccounting(): boolean {
return this.accounting.isCashOrAccrualAccountingRuleId(
this.DailyDepositSchemeProductAccountingForm.value.accountingRule
);
}
isAccrualAccounting(): boolean {
return this.accounting.isAccrualAccountingRuleId(this.DailyDepositSchemeProductAccountingForm.value.accountingRule);
}
}
@@ -0,0 +1,85 @@
<!--
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="rdpch-section">
<div class="rdpch-section__header">
<i class="fa fa-file-invoice-dollar"></i>
<h3>{{ 'labels.heading.Charges' | translate }}</h3>
</div>
<div class="rdpch-section__body">
<div class="rdpch-add-row">
<mat-form-field appearance="outline" class="rdpch-add-row__select">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select #charge>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
<mat-option [value]="charge">
{{ charge.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<button
type="button"
mat-raised-button
color="primary"
class="rdpch-add-btn"
(click)="addCharge(charge)"
[disabled]="!charge.value"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargesDataSource.length > 0) {
<div class="rdpch-table-wrapper">
<table mat-table [dataSource]="chargesDataSource">
<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 }}
</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 }}
</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="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div>
</div>
@@ -0,0 +1,69 @@
/**
* 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/.
*/
.rdpch-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
i {
font-size: 14px;
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
}
&__body {
padding: 16px 24px;
}
}
.rdpch-add-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
&__select {
flex: 1;
min-width: 200px;
}
}
.rdpch-add-btn {
height: 36px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}
.rdpch-table-wrapper {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin-top: 12px;
table {
width: 100%;
}
}
@@ -0,0 +1,103 @@
/**
* 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, inject } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ChargesFilterPipe } from '../../../../pipes/charges-filter.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-charges-step',
templateUrl: './daily-deposit-scheme-product-charges-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-charges-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
ChargesFilterPipe
]
})
export class DailyDepositSchemeProductChargesStepComponent implements OnInit {
dialog = inject(MatDialog);
private translateService = inject(TranslateService);
@Input() DailyDepositSchemeProductsTemplate: any;
@Input() currencyCode: UntypedFormControl;
chargeData: any;
chargesDataSource: {}[];
displayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'action'
];
ngOnInit() {
this.chargeData = this.DailyDepositSchemeProductsTemplate.chargeOptions;
if (!(this.DailyDepositSchemeProductsTemplate === undefined) && this.DailyDepositSchemeProductsTemplate.id) {
this.chargesDataSource = this.DailyDepositSchemeProductsTemplate.charges;
} else {
this.chargesDataSource = [];
}
this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = []));
}
addCharge(charge: any) {
this.chargesDataSource = this.chargesDataSource.concat([charge.value]);
charge.value = '';
}
deleteCharge(charge: any) {
const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: this.translateService.instant('labels.inputs.Charge') + ' ' + charge.name }
});
deleteChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
}
get DailyDepositSchemeProductCharges() {
return {
charges: this.chargesDataSource
};
}
}
@@ -0,0 +1,89 @@
<!--
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/.
-->
<form [formGroup]="DailyDepositSchemeProductCurrencyForm">
<div class="rdpc-section">
<div class="rdpc-section__header">
<i class="fas fa-coins"></i>
<span>Currency &amp; Account Status Configuration</span>
</div>
<div class="rdpc-body">
<div class="rdpc-grid">
<mat-form-field appearance="outline" class="rdpc-field">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<mat-select
formControlName="currencyCode"
matTooltip="{{ 'tooltips.The currency in which the deposit will be disbursed' | translate }}"
required
>
@for (currency of currencyData; track currency) {
<mat-option [value]="currency.code">
{{ currency.name }}
</mat-option>
}
</mat-select>
<mat-hint>Default: Indian Rupee - auto-set for domestic products</mat-hint>
<mat-error>
{{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<mat-select
formControlName="digitsAfterDecimal"
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
required
>
@for (places of decimalPlacesData; track places) {
<mat-option [value]="places">{{ places }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="dormancyAlert">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-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>
<mat-radio-group class="rdpc-alert__choices" formControlName="nominationFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="autoDebitFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
</form>
@@ -0,0 +1,102 @@
/**
* 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;
.rdpc-section {
border: 1px solid $border;
border-radius: 10px;
background: #fff;
& + & {
margin-top: 16px;
}
&__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;
}
}
}
.rdpc-body {
background: #fff;
}
.rdpc-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.rdpc-field {
width: 100%;
}
.rdpc-alert {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 18px 24px 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;
}
}
&__choices {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
}
}
@@ -0,0 +1,104 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-currency-step',
templateUrl: './daily-deposit-scheme-product-currency-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-currency-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
MatRadioGroup,
MatRadioButton
]
})
export class DailyDepositSchemeProductCurrencyStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
@Input() DailyDepositSchemeProductsTemplate: any;
DailyDepositSchemeProductCurrencyForm: UntypedFormGroup;
currencyData: any;
decimalPlacesData = [
2,
0,
3
];
constructor() {
this.createDailyDepositSchemeProductCurrencyForm();
}
ngOnInit() {
this.currencyData = this.DailyDepositSchemeProductsTemplate.currencyOptions.filter(
(c: { code: string }) => c.code === 'INR'
);
this.DailyDepositSchemeProductCurrencyForm.patchValue({
currencyCode: this.DailyDepositSchemeProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
digitsAfterDecimal:
this.DailyDepositSchemeProductsTemplate.digitsAfterDecimal ??
this.DailyDepositSchemeProductsTemplate.currency?.decimalPlaces ??
2,
dormancyAlert: this.DailyDepositSchemeProductsTemplate.dormancyAlert ?? false,
isDormancyTrackingActive: this.DailyDepositSchemeProductsTemplate.isDormancyTrackingActive ?? false,
daysToInactive: this.DailyDepositSchemeProductsTemplate.daysToInactive ?? 30,
daysToDormancy: this.DailyDepositSchemeProductsTemplate.daysToDormancy ?? 60,
daysToEscheat: this.DailyDepositSchemeProductsTemplate.daysToEscheat ?? 90,
nominationFacility: this.DailyDepositSchemeProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.DailyDepositSchemeProductsTemplate.autoDebitFacility ?? false
});
}
createDailyDepositSchemeProductCurrencyForm() {
this.DailyDepositSchemeProductCurrencyForm = this.formBuilder.group({
currencyCode: [
'',
Validators.required
],
digitsAfterDecimal: [
2,
[
Validators.required,
Validators.min(0)
]
],
dormancyAlert: [false],
isDormancyTrackingActive: [false],
daysToInactive: [30],
daysToDormancy: [60],
daysToEscheat: [90],
nominationFacility: [false],
autoDebitFacility: [false]
});
}
get DailyDepositSchemeProductCurrency() {
const formValue = this.DailyDepositSchemeProductCurrencyForm.value;
const result: any = {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
};
if (formValue.dormancyAlert) {
result.daysToInactive = formValue.daysToInactive;
result.daysToDormancy = formValue.daysToDormancy;
result.daysToEscheat = formValue.daysToEscheat;
}
return result;
}
}
@@ -0,0 +1,77 @@
<!--
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/.
-->
<form [formGroup]="DailyDepositSchemeProductDetailsForm">
<div class="rdpd-section">
<div class="rdpd-section__header">
<i class="fa fa-info-circle"></i>
<h3>Account Type Creation</h3>
</div>
<div class="rdpd-grid">
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>Account Type Group</mat-label>
<input matInput formControlName="accountTypeGroup" required />
@if (DailyDepositSchemeProductDetailsForm.controls.accountTypeGroup.hasError('required')) {
<mat-error>Account Type Group <strong>is required</strong></mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>A/c Type</mat-label>
<input matInput formControlName="shortName" maxlength="4" required />
@if (DailyDepositSchemeProductDetailsForm.controls.shortName.hasError('required')) {
<mat-error>A/c Type <strong>is required</strong></mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field rdpd-field--full">
<mat-label>A/c Type Description</mat-label>
<input matInput formControlName="name" required />
@if (DailyDepositSchemeProductDetailsForm.controls.name.hasError('required')) {
<mat-error>A/c Type Description <strong>is required</strong></mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>Interest Rate (%)</mat-label>
<input matInput type="number" formControlName="interestRate" required />
@if (DailyDepositSchemeProductDetailsForm.controls.interestRate.hasError('required')) {
<mat-error>Interest Rate <strong>is required</strong></mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>Auto Interest Post</mat-label>
<mat-select formControlName="autoInterestPost">
@for (option of yesNoOptions; track option.value) {
<mat-option [value]="option.value">{{ option.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (DailyDepositSchemeProductDetailsForm.get('autoInterestPost')?.value) {
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>Auto Interest Post When</mat-label>
<mat-select formControlName="autoInterestPostWhen" required>
@for (option of autoInterestPostWhenData; track option.id) {
<mat-option [value]="option.id">{{ option.value }}</mat-option>
}
</mat-select>
@if (DailyDepositSchemeProductDetailsForm.controls.autoInterestPostWhen.hasError('required')) {
<mat-error>Auto Interest Post When <strong>is required</strong></mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>Auto Interest Post Day</mat-label>
<input matInput type="number" formControlName="autoInterestPostDay" />
</mat-form-field>
}
</div>
</div>
</form>
@@ -0,0 +1,81 @@
/**
* 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;
.rdpd-section {
border: 1px solid $border;
border-radius: 10px;
background: #fff;
& + & {
margin-top: 16px;
}
&__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;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.rdpd-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.rdpd-grid--classification {
grid-template-columns: repeat(3, 1fr);
@media (width <= 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.rdpd-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.rdpd-checkbox-row {
display: flex;
align-items: center;
min-height: 56px;
}
@@ -0,0 +1,177 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-details-step',
templateUrl: './daily-deposit-scheme-product-details-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-details-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
CdkTextareaAutosize
]
})
export class DailyDepositSchemeProductDetailsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private cdr = inject(ChangeDetectorRef);
@Input() DailyDepositSchemeProductsTemplate: any;
DailyDepositSchemeProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = [];
customerSegmentData: string[] = [];
liabilityAccountOptions: any[] = [];
/** "Auto Interest Post When" options - the legacy screen's own field, sourced from the same
* enum the (hidden) interest-posting-period setting uses, so the two always stay in sync. */
autoInterestPostWhenData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() {
this.createDailyDepositSchemeProductDetailsForm();
}
ngOnInit() {
if (this.DailyDepositSchemeProductsTemplate) {
this.applyTemplate(this.DailyDepositSchemeProductsTemplate);
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.DailyDepositSchemeProductsTemplate?.currentValue) {
this.applyTemplate(changes.DailyDepositSchemeProductsTemplate.currentValue);
}
}
createDailyDepositSchemeProductDetailsForm() {
this.DailyDepositSchemeProductDetailsForm = this.formBuilder.group({
accountTypeGroup: [
'',
Validators.required
],
name: [
'',
Validators.required
],
shortName: [
'',
Validators.required
],
description: [''],
autoInterestPost: [true],
autoInterestPostWhen: [
'',
Validators.required
],
autoInterestPostDay: [''],
interestRate: [
2,
[
Validators.required,
Validators.min(0)
]
],
accountType: [
null,
Validators.required
],
glAccountId: [null],
effectiveTo: [''],
endDate: [''],
introducerRequired: [false],
shareLinkingEnabled: [false]
});
}
get minEndDate(): Date | null {
const v = this.DailyDepositSchemeProductDetailsForm.get('effectiveTo')?.value;
if (!v) return null;
const d = new Date(v);
d.setDate(d.getDate() + 1);
return d;
}
private getOptionNames(options: any[]): string[] {
if (!Array.isArray(options)) {
return [];
}
return options
.map((option) => option?.name ?? option?.value ?? option)
.filter((value) => typeof value === 'string' && value.trim().length > 0);
}
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) {
return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
}
if (dateValue instanceof Date) {
return dateValue;
}
return new Date(dateValue);
}
compareById(o1: any, o2: any): boolean {
return o1 != null && o2 != null && Number(o1) === Number(o2);
}
private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? [];
this.autoInterestPostWhenData = template.interestPostingPeriodTypeOptions ?? [];
this.cdr.detectChanges();
const isEditing = !!template.id;
this.DailyDepositSchemeProductDetailsForm.patchValue({
accountTypeGroup: template.accountTypeGroup || 'DDS',
name: isEditing ? template.name : template.name || '',
shortName: isEditing ? template.shortName : template.shortName || '',
description: template.description || '',
autoInterestPost: template.interestPostingPeriodType ? true : (template.autoInterestPost ?? true),
autoInterestPostWhen: template.interestPostingPeriodType?.id ?? this.autoInterestPostWhenData[0]?.id ?? '',
autoInterestPostDay: template.autoInterestPostDay ?? template.frequencyFixDay ?? '',
interestRate: template.nominalAnnualInterestRate ?? 2,
accountType: template.accountType ?? this.accountTypeData[0] ?? null,
glAccountId: template.glAccountId ?? null,
effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate),
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
shareLinkingEnabled: !!template.shareLinkingEnabled
});
}
get DailyDepositSchemeProductDetails() {
const formValue = { ...this.DailyDepositSchemeProductDetailsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveTo instanceof Date) {
formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || '';
}
if (formValue.endDate instanceof Date) {
formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || '';
}
// accountTypeGroup / autoInterestPost / autoInterestPostWhen are the legacy screen's own
// fields - they don't map to a real product API parameter by that name, so they're folded
// into the actual payload fields (description, autoPost, interestPostingPeriodType) by the
// parent component rather than sent through as-is.
return formValue;
}
}
@@ -0,0 +1,109 @@
<!--
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/.
-->
<!-- ── Bank Interest Rate Chart (editable reference table from DB) ───────── -->
<div class="rdpi-section rdpi-ref-section">
<div class="rdpi-section__header">
<i class="fa fa-table"></i>
<h3>Bank Interest Rate Chart &nbsp;<span class="rdpi-ref-badge">Reference / Editable</span></h3>
<span class="rdpi-spacer"></span>
<button type="button" mat-raised-button color="primary" class="rdpi-add-btn" (click)="addSlab()">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
Add Slab
</button>
</div>
@if (_referenceVersionLabel) {
<div class="rdpi-ref-meta">
<span><strong>Version:</strong> {{ _referenceVersionLabel }}</span>
@if (_referenceRemarks) {
<span>&nbsp;| {{ _referenceRemarks }}</span>
}
</div>
}
@if (_editableSlabs.length === 0) {
<div class="rdpi-empty-msg">
<h3>No interest rate slabs defined. Click <strong>Add Slab</strong> to add one.</h3>
</div>
}
@if (_editableSlabs.length > 0) {
<div class="rdpi-table-wrapper">
<table mat-table [dataSource]="_editableSlabs" class="rdpi-ref-table">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input"
[value]="slab.description"
(change)="editSlab(i, 'description', $any($event.target).value)"
/>
</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input rdpi-inline-input--num"
type="number"
[value]="slab.fromPeriod"
(change)="editSlab(i, 'fromPeriod', +$any($event.target).value)"
/>
</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input rdpi-inline-input--num"
type="number"
[value]="slab.toPeriod ?? ''"
(change)="editSlab(i, 'toPeriod', $any($event.target).value ? +$any($event.target).value : null)"
/>
</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input rdpi-inline-input--rate"
type="number"
step="0.01"
[value]="slab.annualInterestRate"
(change)="editSlab(i, 'annualInterestRate', +$any($event.target).value)"
/>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="refSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: refSlabColumns"></tr>
</table>
</div>
<div class="rdpi-ref-footer">
<p class="rdpi-ref-hint">
Edit the rates above. When you <strong>Save</strong> the product, a new version will be inserted in the database
— previous rates are preserved as history.
</p>
</div>
}
</div>
@@ -0,0 +1,215 @@
/**
* 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/.
*/
.rdpi-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
margin-top: 16px;
}
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
i {
font-size: 14px;
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
}
}
.rdpi-spacer {
flex: 1;
}
.rdpi-content {
padding: 16px 24px;
}
.rdpi-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
}
.rdpi-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.rdpi-checkbox-row {
display: flex;
align-items: center;
padding: 4px 0;
}
.rdpi-add-btn {
height: 36px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}
.rdpi-chart-block {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
background: #fafbfc;
}
.rdpi-chart-actions {
display: flex;
justify-content: flex-end;
}
.rdpi-slab-actions {
display: flex;
justify-content: center;
padding: 8px 0 16px;
}
.rdpi-empty-msg {
text-align: center;
padding: 16px;
h3 {
margin: 0;
font-size: 14px;
font-weight: 500;
color: #5f6368;
}
}
.rdpi-table-wrapper {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin-top: 8px;
table {
width: 100%;
}
tr.incentives-row {
height: 0;
> td {
padding: 0;
}
}
}
.rdpi-incentives {
overflow: hidden;
}
.rdpi-incentive-card {
border-radius: 0;
padding: 16px;
}
.rdpi-incentive-title {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
color: #202124;
}
.rdpi-incentive-add {
margin-bottom: 12px;
}
@media (width <= 600px) {
.rdpi-grid {
grid-template-columns: 1fr;
}
}
/* ── Editable reference-rate table ───────────────────────────── */
.rdpi-ref-section {
margin-top: 24px;
border-color: #bfdbfe;
background: #f0f7ff;
.rdpi-section__header {
background: #dbeafe;
color: #1e3a5f;
}
}
.rdpi-ref-badge {
font-size: 11px;
font-weight: 500;
background: #3b82f6;
color: #fff;
border-radius: 10px;
padding: 2px 8px;
vertical-align: middle;
}
.rdpi-ref-meta {
padding: 8px 16px;
font-size: 13px;
color: #555;
border-bottom: 1px solid #bfdbfe;
}
.rdpi-ref-table {
width: 100%;
}
.rdpi-inline-input {
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 3px 6px;
font-size: 13px;
width: 100%;
background: #fff;
&:focus {
outline: none;
border-color: #3b82f6;
}
&--num {
width: 70px;
}
&--rate {
width: 80px;
}
}
.rdpi-ref-footer {
padding: 10px 16px;
}
.rdpi-ref-hint {
font-size: 12px;
color: #64748b;
margin: 0;
}
@@ -0,0 +1,717 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, OnChanges, Input, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
Validators,
UntypedFormArray,
ReactiveFormsModule
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { animate, state, style, transition, trigger } from '@angular/animations';
/** Custom Components */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { DepositProductIncentiveFormDialogComponent } from 'app/products/deposit-product-incentive-form-dialog/deposit-product-incentive-form-dialog.component';
/** Dialog Components */
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { TranslateService } from '@ngx-translate/core';
import { MatButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import { MatCheckbox } from '@angular/material/checkbox';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FindPipe } from '../../../../pipes/find.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-interest-rate-chart-step',
templateUrl: './daily-deposit-scheme-product-interest-rate-chart-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-interest-rate-chart-step.component.scss'],
animations: [
trigger('expandChartSlab', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDivider,
MatButton,
MatCheckbox,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FindPipe
]
})
export class DailyDepositSchemeProductInterestRateChartStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
dialog = inject(MatDialog);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private translateService = inject(TranslateService);
@Input() DailyDepositSchemeProductsTemplate: any;
/** Reference rate chart fetched from m_rd_interest_rate_effective / m_rd_interest_rate_slab_history */
@Input() set rdInterestRates(rates: any[]) {
if (rates && rates.length > 0) {
// Use the first active version's slabs as the editable working copy
const activeVersion = rates.find((v: any) => v.isActive) ?? rates[0];
this._referenceVersionLabel = activeVersion.versionLabel;
this._referenceRemarks = activeVersion.remarks;
// Deep-clone slabs so the original is not mutated
this._editableSlabs = (activeVersion.slabs ?? []).map((s: any) => ({ ...s }));
this.ensureSlabRangeRules(this._editableSlabs);
}
}
/** Slabs the user is editing in the reference-rate table */
_editableSlabs: any[] = [];
_referenceVersionLabel = '';
_referenceRemarks = '';
/** Columns for the editable reference-rate table */
readonly refSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
/** Returns the payload to save as a new rate version (called by parent after product save) */
get modifiedRdInterestRatePayload(): any {
return {
versionLabel: this._referenceVersionLabel || 'Rate Version ' + new Date().toISOString().slice(0, 10),
remarks: this._referenceRemarks,
slabs: this._editableSlabs.map((s: any, idx: number) => ({
srNo: s.srNo ?? idx + 1,
description: s.description,
periodTypeEnum: s.periodTypeEnum ?? 1,
fromPeriod: s.fromPeriod,
toPeriod: s.toPeriod ?? null,
annualInterestRate: s.annualInterestRate
}))
};
}
editSlab(index: number, field: string, value: any) {
const slabs = this._editableSlabs.map((slab: any) => ({ ...slab }));
const current = slabs[index];
if (!current) {
return;
}
const nextValue = this.normalizeSlabValue(value);
const oldFrom = this.normalizeSlabValue(current.fromPeriod);
const oldTo = this.normalizeSlabValue(current.toPeriod);
current[field] = nextValue;
// Keep the first slab anchored at 0 so the chart always starts from zero.
if (index === 0 && field === 'fromPeriod') {
current.fromPeriod = 0;
}
if (field === 'fromPeriod' && nextValue === null) {
current.fromPeriod = oldFrom;
}
if (field === 'fromPeriod') {
const effectiveFrom = this.normalizeSlabValue(current.fromPeriod);
const delta = effectiveFrom - oldFrom;
if (current.toPeriod !== null && current.toPeriod !== undefined) {
current.toPeriod = this.normalizeSlabValue(current.toPeriod) + delta;
}
this.shiftFollowingSlabs(slabs, index, delta);
}
if (field === 'toPeriod') {
if (index < slabs.length - 1 && nextValue === null) {
// Non-final slabs should always have a closing boundary so later slabs can stay contiguous.
current.toPeriod = oldTo;
} else {
const delta = (this.normalizeSlabValue(current.toPeriod) ?? 0) - (oldTo ?? 0);
this.shiftFollowingSlabs(slabs, index, delta);
}
}
this.ensureSlabRangeRules(slabs);
this._editableSlabs = slabs;
}
addSlab() {
const slabs = this._editableSlabs.map((slab: any) => ({ ...slab }));
const nextSr = slabs.length + 1;
const lastSlab = slabs[slabs.length - 1];
const lastFrom = this.normalizeSlabValue(lastSlab?.fromPeriod) ?? 0;
if (lastSlab && this.normalizeSlabValue(lastSlab.toPeriod) === null) {
// Split the open-ended tail so the new slab can become the new last row.
// This keeps the sequence contiguous while preserving the existing tail start.
lastSlab.toPeriod = lastFrom;
}
const previousSlab = slabs[slabs.length - 1];
this._editableSlabs = [
...slabs,
{
srNo: nextSr,
description: '',
periodTypeEnum: 1,
periodTypeLabel: 'Days',
fromPeriod: slabs.length === 0 ? 0 : this.nextFromPeriod(previousSlab),
toPeriod: null,
annualInterestRate: null
}
];
this.ensureSlabRangeRules(this._editableSlabs);
}
private normalizeSlabValue(value: any): number | null {
if (value === null || value === undefined || value === '') {
return null;
}
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
}
private nextFromPeriod(previousSlab: any): number {
const previousTo = this.normalizeSlabValue(previousSlab?.toPeriod);
const previousFrom = this.normalizeSlabValue(previousSlab?.fromPeriod) ?? 0;
return previousTo === null ? previousFrom + 1 : previousTo + 1;
}
private shiftFollowingSlabs(slabs: any[], index: number, delta: number): void {
if (!delta) {
return;
}
for (let i = index + 1; i < slabs.length; i++) {
const slab = slabs[i];
const currentFrom = this.normalizeSlabValue(slab.fromPeriod);
const currentTo = this.normalizeSlabValue(slab.toPeriod);
if (currentFrom !== null) {
slab.fromPeriod = currentFrom + delta;
}
if (currentTo !== null) {
slab.toPeriod = currentTo + delta;
}
}
}
private ensureSlabRangeRules(slabs: any[]): void {
if (slabs.length === 0) {
return;
}
slabs[0].fromPeriod = 0;
for (let i = 1; i < slabs.length; i++) {
const previous = slabs[i - 1];
const previousTo = this.normalizeSlabValue(previous.toPeriod);
if (previousTo === null) {
slabs[i].fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod);
continue;
}
slabs[i].fromPeriod = previousTo + 1;
}
// Keep the last slab open-ended so the chart ends with null.
slabs[slabs.length - 1].toPeriod = null;
// Prevent inverted ranges on intermediate rows.
for (let i = 0; i < slabs.length - 1; i++) {
const fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod);
const toPeriod = this.normalizeSlabValue(slabs[i].toPeriod);
if (fromPeriod !== null && toPeriod !== null && toPeriod < fromPeriod) {
slabs[i].toPeriod = fromPeriod;
}
}
}
DailyDepositSchemeProductInterestRateChartForm: UntypedFormGroup;
periodTypeData: any;
entityTypeData: any;
attributeNameData: any;
conditionTypeData: any;
genderData: any;
clientTypeData: any;
clientClassificationData: any;
incentiveTypeData: any;
chartSlabsDisplayedColumns: any[] = [];
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
incentivesDisplayedColumns: string[] = [
'entityType',
'attributeName',
'conditionType',
'attributeValue',
'incentiveType',
'amount',
'actions'
];
minDate = new Date(2000, 0, 1);
maxDate = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
expandChartSlabIndex: number[] = [];
chartDetailData: any = [];
chartsDetail: any[] = [];
/**
* @param {FormBuilder} formBuilder Form Builder.
* @param {MatDialog} dialog Dialog reference.
* @param {Dates} dateUtils Date Utils to format date.
* @param {SettingsService} settingsService Settings Service.
*/
constructor() {
this.createDailyDepositSchemeProductInterestRateChartForm();
}
ngOnInit() {
this.periodTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.periodTypes;
this.entityTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.entityTypeOptions;
this.attributeNameData = this.DailyDepositSchemeProductsTemplate.chartTemplate.attributeNameOptions;
this.conditionTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.conditionTypeOptions;
this.genderData = this.DailyDepositSchemeProductsTemplate.chartTemplate.genderOptions;
this.clientTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.clientTypeOptions;
this.clientClassificationData = this.DailyDepositSchemeProductsTemplate.chartTemplate.clientClassificationOptions;
this.incentiveTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.incentiveTypeOptions;
// assignFormData() intentionally not called — old chart form is removed from UI.
// The DailyDepositSchemeProductInterestRateChartForm is kept only for stepControl
// validity; it remains empty (valid) so the preview tab becomes available.
}
// Assign form the values for edit Daily Deposit Scheme
assignFormData() {
this.addChart();
const isChartArray = Array.isArray(this.DailyDepositSchemeProductsTemplate.activeChart);
if (this.DailyDepositSchemeProductsTemplate.activeChart) {
if (!isChartArray) {
this.chartDetailData.push(this.DailyDepositSchemeProductsTemplate.activeChart);
} else {
this.chartDetailData = this.DailyDepositSchemeProductsTemplate.activeChart;
}
}
// Build the array of Objects from the retrived value
this.getChartsDetailsData();
// Iterates for every chart in charts
this.charts.controls.forEach((chartDetailControl: UntypedFormGroup, i: number) => {
if (!this.chartsDetail[i]) {
return;
}
// Iterate for every chartSlab in chart
this.chartsDetail[i].chartSlabs.forEach((chartSlabDetail: any, j: number) => {
const chartSlabInfo = this.formBuilder.group({
id: [chartSlabDetail.id],
amountRangeFrom: [chartSlabDetail.amountRangeFrom],
amountRangeTo: [chartSlabDetail.amountRangeTo],
annualInterestRate: [
chartSlabDetail.annualInterestRate,
Validators.required
],
description: [
chartSlabDetail.description,
Validators.required
],
fromPeriod: [
chartSlabDetail.fromPeriod,
Validators.required
],
toPeriod: [chartSlabDetail.toPeriod],
periodType: [
chartSlabDetail.periodType,
Validators.required
],
incentives: this.formBuilder.array([])
});
const formArray = chartDetailControl.controls['chartSlabs'] as UntypedFormArray;
formArray.push(chartSlabInfo);
// Iterate for every slab in chartSlab
const chartIncentiveControl = (chartDetailControl.controls['chartSlabs'] as UntypedFormArray).controls[j];
// Iterate to input all the incentive for particular chart slab
this.chartsDetail[i].chartSlabs[j].incentives.forEach((chartIncentiveDetail: any) => {
const incentiveInfo = this.formBuilder.group({
amount: [
chartIncentiveDetail.amount,
Validators.required
],
attributeName: [
chartIncentiveDetail.attributeName,
Validators.required
],
attribureValue: [
chartIncentiveDetail.attribureValue,
Validators.required
],
conditionType: [
chartIncentiveDetail.conditionType,
Validators.required
],
entityType: [
chartIncentiveDetail.entityType,
Validators.required
],
incentiveType: [
chartIncentiveDetail.incentiveType,
Validators.required
]
});
const newFormArray = (chartIncentiveControl as UntypedFormGroup).controls['incentives'] as UntypedFormArray;
newFormArray.push(incentiveInfo);
});
});
});
}
getChartsDetailsData() {
this.chartDetailData.forEach((chartData: any) => {
const chart: {
id?: any;
endDate: string | Date;
fromDate: string | Date;
isPrimaryGroupingByAmount: any;
name: any;
chartSlabs: any[];
} = {
endDate: chartData.endDate ? new Date(chartData.endDate) : '',
fromDate: chartData.fromDate ? new Date(chartData.fromDate) : '',
isPrimaryGroupingByAmount: chartData.isPrimaryGroupingByAmount,
name: chartData.name,
chartSlabs: this.getChartSlabsData(chartData)
};
if (chartData.id) {
chart['id'] = chartData.id;
}
this.chartsDetail.push(chart);
});
this.DailyDepositSchemeProductInterestRateChartForm.patchValue({
charts: this.chartsDetail
});
}
getChartSlabsData(chartData: any) {
const chartSlabs: any[] = [];
let chartSlabData: any[] = [];
const isChartSlabArray = Array.isArray(chartData.chartSlabs);
if (!isChartSlabArray) {
chartSlabData.push(chartData.chartSlabs);
} else {
chartSlabData = chartData.chartSlabs;
}
chartSlabData.forEach((eachChartSlabData: any) => {
const chartSlab: {
id?: any;
periodType: any;
amountRangeFrom: any;
amountRangeTo: any;
annualInterestRate: any;
description: any;
fromPeriod: any;
toPeriod: any;
incentives: any[];
} = {
periodType: eachChartSlabData.periodType.id,
amountRangeFrom: eachChartSlabData.amountRangeFrom,
amountRangeTo: eachChartSlabData.amountRangeTo,
annualInterestRate: eachChartSlabData.annualInterestRate,
description: eachChartSlabData.description ? eachChartSlabData.description : '',
fromPeriod: eachChartSlabData.fromPeriod,
toPeriod: eachChartSlabData.toPeriod,
incentives: this.getIncentivesData(chartSlabData)
};
if (eachChartSlabData.id) {
chartSlab['id'] = eachChartSlabData.id;
}
chartSlabs.push(chartSlab);
});
return chartSlabs;
}
getIncentivesData(chartSlabData: any) {
const incentives: any[] = [];
let incentiveDatas: any[] = [];
if (chartSlabData.incentives) {
const isChartIncentiveArray = Array.isArray(chartSlabData.incentives);
if (!isChartIncentiveArray) {
incentiveDatas.push(chartSlabData.incentives);
} else {
incentiveDatas = chartSlabData.incentives;
}
incentiveDatas.forEach((incentiveData: any) => {
const incentive = {
amount: incentiveData.amount,
attributeName: incentiveData.attributeName,
attributeValue: incentiveData.attributeValue,
conditionType: incentiveData.conditionType,
entityType: incentiveData.entityType,
incentiveType: incentiveData.incentiveType
};
incentives.push(incentive);
});
}
return incentives;
}
createDailyDepositSchemeProductInterestRateChartForm() {
this.DailyDepositSchemeProductInterestRateChartForm = this.formBuilder.group({
charts: this.formBuilder.array([])
});
}
get charts(): UntypedFormArray {
return this.DailyDepositSchemeProductInterestRateChartForm.get('charts') as UntypedFormArray;
}
createChartForm(): UntypedFormGroup {
return this.formBuilder.group({
id: [null],
name: [''],
description: [''],
fromDate: [
'',
Validators.required
],
endDate: [''],
isPrimaryGroupingByAmount: [false],
chartSlabs: this.formBuilder.array([], Validators.required)
});
}
addChart() {
this.charts.push(this.createChartForm());
this.setConditionalControls(this.charts.length - 1);
}
setConditionalControls(chartIndex: number) {
this.chartSlabsDisplayedColumns[chartIndex] = [
'period',
'amountRange',
'annualInterestRate',
'description',
'actions'
];
this.charts
.at(chartIndex)
.get('isPrimaryGroupingByAmount')
.valueChanges.subscribe((isPrimaryGroupingByAmount: boolean) => {
this.chartSlabsDisplayedColumns[chartIndex] = isPrimaryGroupingByAmount ? [
'amountRange',
'period'
] : [
'period',
'amountRange'
];
this.chartSlabsDisplayedColumns[chartIndex].push('annualInterestRate', 'description', 'actions');
});
}
getIncentives(chartSlabs: UntypedFormArray, chartSlabIndex: number): UntypedFormArray {
return chartSlabs.at(chartSlabIndex).get('incentives') as UntypedFormArray;
}
addChartSlab(chartSlabs: UntypedFormArray) {
const data = { ...this.getData('Slab') };
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
response.data.addControl('incentives', this.formBuilder.array([]));
chartSlabs.push(response.data);
}
});
}
addIncentive(incentives: UntypedFormArray) {
const data = { ...this.getData('Incentive'), entityType: this.entityTypeData[0].id };
const dialogRef = this.dialog.open(DepositProductIncentiveFormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
incentives.push(response.data);
}
});
}
editChartSlab(chartSlabs: UntypedFormArray, chartSlabIndex: number) {
const data = { ...this.getData('Slab', chartSlabs.at(chartSlabIndex).value), layout: { addButtonText: 'Edit' } };
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
chartSlabs.at(chartSlabIndex).patchValue(response.data.value);
}
});
}
editIncentive(incentives: UntypedFormArray, incentiveIndex: number) {
const data = {
...this.getData('Incentive', incentives.at(incentiveIndex).value),
layout: { addButtonText: 'Edit' }
};
const dialogRef = this.dialog.open(DepositProductIncentiveFormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
incentives.at(incentiveIndex).patchValue(response.data.value);
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case 'Slab':
return {
title: this.translateService.instant('labels.inputs.Slab'),
formfields: this.getSlabFormfields(values)
};
case 'Incentive':
return { values, chartTemplate: this.DailyDepositSchemeProductsTemplate.chartTemplate };
}
}
getSlabFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'periodType',
label: this.translateService.instant('labels.inputs.Period Type'),
value: values ? values.periodType : this.periodTypeData[0].id,
options: { label: 'value', value: 'id', data: this.periodTypeData },
required: true,
order: 1
}),
new InputBase({
controlName: 'fromPeriod',
label: this.translateService.instant('labels.inputs.Period From'),
value: values ? values.fromPeriod : undefined,
type: 'number',
required: true,
order: 2,
min: 0
}),
new InputBase({
controlName: 'toPeriod',
label: this.translateService.instant('labels.inputs.Period To'),
value: values ? values.toPeriod : undefined,
type: 'number',
order: 3,
min: 0
}),
new InputBase({
controlName: 'amountRangeFrom',
label: this.translateService.instant('labels.inputs.Amount Range From'),
value: values ? values.amountRangeFrom : undefined,
type: 'number',
order: 4,
min: 0
}),
new InputBase({
controlName: 'amountRangeTo',
label: this.translateService.instant('labels.inputs.Amount Range To'),
value: values ? values.amountRangeTo : undefined,
type: 'number',
order: 5,
min: 0
}),
new InputBase({
controlName: 'annualInterestRate',
label: this.translateService.instant('labels.inputs.Interest'),
value: values ? values.annualInterestRate : undefined,
type: 'number',
required: true,
order: 6,
min: 0
}),
new InputBase({
controlName: 'description',
label: this.translateService.instant('labels.inputs.Description'),
value: values ? values.description : undefined,
required: true,
order: 7
})
];
return formfields;
}
get DailyDepositSchemeProductInterestRateChart() {
// TODO: Update once language and date settings are setup
const dateFormat = 'YYYY-MM-DD';
const locale = this.settingsService.language.code;
const DailyDepositSchemeProductInterestRateChart = this.DailyDepositSchemeProductInterestRateChartForm.value;
for (const chart of DailyDepositSchemeProductInterestRateChart.charts) {
chart.locale = locale;
chart.dateFormat = 'yyyy-MM-dd';
if (chart.fromDate instanceof Date) {
chart.fromDate = this.dateUtils.formatDateAsString(chart.fromDate, dateFormat);
}
if (chart.endDate) {
if (chart.endDate instanceof Date) {
chart.endDate = this.dateUtils.formatDateAsString(chart.endDate, dateFormat);
}
}
if (chart.endDate === '') {
delete chart.endDate;
}
if (chart.description === '') {
delete chart.description;
}
if (chart.id === null) {
delete chart.id;
}
}
return {
...DailyDepositSchemeProductInterestRateChart,
rdSlabs: this._editableSlabs
};
}
}
@@ -0,0 +1,732 @@
<!--
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="responsive-column">
<!-- ── Product Name ── -->
<h2 class="mat-h2 flex-100">{{ DailyDepositSchemeProduct.name }}</h2>
<!-- ── Details ── -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.shortName }}</span>
</div>
@if (DailyDepositSchemeProduct.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.description }}</span>
</div>
}
<!-- ── Product Classification ── -->
@if (
DailyDepositSchemeProduct.accountType ||
DailyDepositSchemeProduct.customerSegment ||
DailyDepositSchemeProduct.glAccountId ||
DailyDepositSchemeProduct.effectiveTo ||
DailyDepositSchemeProduct.introducerRequired !== undefined ||
DailyDepositSchemeProduct.shareLinkingEnabled !== undefined
) {
<h3 class="mat-h3 flex-100">Product Classification</h3>
<mat-divider class="flex-100"></mat-divider>
@if (DailyDepositSchemeProduct.accountType) {
<div class="flex-100 layout-row">
<span class="flex-40">Account Type:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.accountType }}</span>
</div>
}
@if (DailyDepositSchemeProduct.glAccountId) {
<div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span>
<span class="flex-60">{{ getGlAccountName(DailyDepositSchemeProduct.glAccountId) }}</span>
</div>
}
@if (DailyDepositSchemeProduct.effectiveTo) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.effectiveTo | dateFormat }}</span>
</div>
}
@if (DailyDepositSchemeProduct.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">End Date:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.endDate | dateFormat }}</span>
</div>
}
@if (DailyDepositSchemeProduct.introducerRequired !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Introducer Required:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.introducerRequired === true ||
DailyDepositSchemeProduct.introducerRequired === 'Yes'
? 'Yes'
: 'No'
}}</span>
</div>
}
@if (DailyDepositSchemeProduct.shareLinkingEnabled !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Deposit Account Link:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.shareLinkingEnabled | yesNo }}</span>
</div>
}
}
<!-- ── Currency ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.currencyCode
| find: DailyDepositSchemeProductsTemplate.currencyOptions : 'code' : 'name'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.digitsAfterDecimal }}</span>
</div>
@if (DailyDepositSchemeProduct.dormancyAlert !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.dormancyAlert | yesNo }}</span>
</div>
}
@if (DailyDepositSchemeProduct.nominationFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.nominationFacility | yesNo }}</span>
</div>
}
@if (DailyDepositSchemeProduct.autoDebitFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Auto Debit Facility:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.autoDebitFacility | yesNo }}</span>
</div>
}
@if (
DailyDepositSchemeProduct.minDepositAmount !== undefined || DailyDepositSchemeProduct.maxDepositAmount !== undefined
) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">Deposit Amount</h3>
<mat-divider class="flex-100"></mat-divider>
@if (DailyDepositSchemeProduct.minDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Minimum:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.minDepositAmount }}</span>
</div>
}
@if (DailyDepositSchemeProduct.maxDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Maximum:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.maxDepositAmount }}</span>
</div>
}
}
<!-- ── Terms ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest Rate' | translate }} (%):</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.interestCompoundingPeriodType
| find: DailyDepositSchemeProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.interestPostingPeriodType
| find: DailyDepositSchemeProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value'
}}</span>
</div>
@if (DailyDepositSchemeProduct.interestCalculateSelector) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.interestCalculateSelector
| find: DailyDepositSchemeProductsTemplate.interestCalculationTypeOption : 'id' : 'value'
}}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.interestCalculationType
| find: DailyDepositSchemeProductsTemplate.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{
DailyDepositSchemeProduct.interestCalculationDaysInYearType
| find: DailyDepositSchemeProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
}}</span>
</div>
@if (DailyDepositSchemeProduct.effectiveDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.effectiveDate | dateFormat }}</span>
</div>
}
@if (DailyDepositSchemeProduct.averagePeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Average Period:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.averagePeriod }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Auto Interest Post:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.autoPost | yesNo }}</span>
</div>
@if (
DailyDepositSchemeProduct.frequencyFixDay !== undefined &&
DailyDepositSchemeProduct.frequencyFixDay !== null &&
DailyDepositSchemeProduct.frequencyFixDay !== ''
) {
<div class="flex-100 layout-row">
<span class="flex-40">Frequency (Fix Day):</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.frequencyFixDay }}</span>
</div>
}
@if (DailyDepositSchemeProduct.lastInterestDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.lastInterestDate | dateFormat }}</span>
</div>
}
<!-- ── Settings ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">Loan Facility:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.loanFacility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Premature Withdrawal:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.prematureWithdrawalOption | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Tenure Flexibility:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.tenureFlexibility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Lien Marked:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.lienMarked | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.withdrawalFeeForTransfers | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Enable Lock-in Period:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.enableLockinPeriod | yesNo }}</span>
</div>
@if (DailyDepositSchemeProduct.enableLockinPeriod && DailyDepositSchemeProduct.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">Lock-in Period:</span>
<span class="flex-60">
{{ DailyDepositSchemeProduct.lockinPeriodFrequency }}
{{ getLockinPeriodFrequencyTypeLabel(DailyDepositSchemeProduct.lockinPeriodFrequencyType) }}
</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Maturity Instructions:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.maturityInstructions | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Credit Maturity Proceeds:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.creditMaturityProceeds | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Penalty Grace Period (Missed Installments):</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.penaltyGracePeriodMissedInstallments | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Panel Request Applicable:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.panelRequestApplicable | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">No Penalty (Nominee / Heir Closure):</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.noPenaltyNomineeHeirClosure | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Deceased Settlement:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.deceasedSettlement | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Allow Debit Txn After Due Date:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.allowDebitTxnAfterDueDate | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Account Closed with Zero Balance in Day Closing:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
@if (DailyDepositSchemeProduct.minBalanceForInterestCalculation) {
<div class="flex-100 layout-row">
<span class="flex-40">Min Balance for Interest Calculation:</span>
<span class="flex-60">{{ DailyDepositSchemeProduct.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
<!-- ── Interest Rate Slabs ── -->
@if (DailyDepositSchemeProduct.rdSlabs?.length) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row-wrap responsive-column flex-100">
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="DailyDepositSchemeProduct.rdSlabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="rdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: rdSlabColumns"></tr>
</table>
</div>
}
<!-- ── Charges ── -->
@if (DailyDepositSchemeProduct.charges?.length) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="DailyDepositSchemeProduct.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 }}</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>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
}
<!-- ── Accounting ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (isCashOrAccrualAccounting()) {
<div class="spa-gl-summary flex-100">
<div class="spa-gl-summary__header">
<h4 class="mat-h4">GL Mapping</h4>
</div>
<div class="spa-gl-section">
<h5 class="mat-h5">Assets</h5>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Savings Reference Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.savingsReferenceAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions
)
}}</span>
</div>
@if (isAccrualAccounting()) {
<div class="spa-gl-row">
<span class="spa-gl-row__label">Fees Receivable Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.feesReceivableAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions
)
}}</span>
</div>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Penalties Receivable Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.penaltiesReceivableAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions
)
}}</span>
</div>
}
</div>
<div class="spa-gl-section">
<h5 class="mat-h5">Liabilities</h5>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Savings Control Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.savingsControlAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
)
}}</span>
</div>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Transfers In Suspense Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.transfersInSuspenseAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
)
}}</span>
</div>
@if (isAccrualAccounting()) {
<div class="spa-gl-row">
<span class="spa-gl-row__label">Interest Payable Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.interestPayableAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
)
}}</span>
</div>
}
</div>
<div class="spa-gl-section">
<h5 class="mat-h5">Expenses</h5>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Interest on Savings Account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.interestOnSavingsAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.expenseAccountOptions
)
}}</span>
</div>
</div>
<div class="spa-gl-section">
<h5 class="mat-h5">Income</h5>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Income from Fees</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.incomeFromFeeAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions
)
}}</span>
</div>
<div class="spa-gl-row">
<span class="spa-gl-row__label">Income from Penalties</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
DailyDepositSchemeProduct.incomeFromPenaltyAccountId,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions
)
}}</span>
</div>
</div>
</div>
}
<!-- Other GL Mappings -->
@if (DailyDepositSchemeProduct.otherGlMappings) {
<div class="spa-other-gl-summary flex-100">
<div class="spa-gl-summary__header">
<h4 class="mat-h4">Other GL Mappings</h4>
</div>
@if (DailyDepositSchemeProduct.otherGlMappings.assets?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="DailyDepositSchemeProduct.otherGlMappings.assets"
>
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Asset Account</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glCode">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">
{{
formatGlAccountByCode(
row.glCode,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions
)
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
</table>
}
@if (DailyDepositSchemeProduct.otherGlMappings.liabilities?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="DailyDepositSchemeProduct.otherGlMappings.liabilities"
>
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Liability Account</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glCode">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">
{{
formatGlAccountByCode(
row.glCode,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
)
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
</table>
}
@if (DailyDepositSchemeProduct.otherGlMappings.expenses?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="DailyDepositSchemeProduct.otherGlMappings.expenses"
>
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Expense Account</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glCode">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">
{{
formatGlAccountByCode(
row.glCode,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.expenseAccountOptions
)
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
</table>
}
@if (DailyDepositSchemeProduct.otherGlMappings.income?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="DailyDepositSchemeProduct.otherGlMappings.income"
>
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Income Account</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glCode">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">
{{
formatGlAccountByCode(
row.glCode,
DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions
)
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
</table>
}
</div>
}
<!-- Advanced Accounting Rules -->
@if (
DailyDepositSchemeProduct.advancedAccountingRules &&
(DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length ||
DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length ||
DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length)
) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="DailyDepositSchemeProduct.paymentChannelToFundSourceMappings"
>
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.paymentTypeId | find: DailyDepositSchemeProductsTemplate.paymentTypeOptions : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{
item.fundSourceAccountId
| find
: DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions
: 'id'
: 'name'
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
@if (DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="DailyDepositSchemeProduct.feeToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.chargeId | find: DailyDepositSchemeProductsTemplate.chargeOptions : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{
item.incomeAccountId
| find
: DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions
: 'id'
: 'name'
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
@if (DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="DailyDepositSchemeProduct.penaltyToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.chargeId | find: DailyDepositSchemeProductsTemplate.penaltyOptions : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{
item.incomeAccountId
| find
: DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions
: 'id'
: 'name'
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
}
</div>
@@ -0,0 +1,131 @@
/**
* 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 'assets/styles/colours' as *;
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
h2,
h3,
h4 {
margin: 0;
font-weight: 500;
}
table {
width: 100%;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
/* ── GL mapping summary (same as savings product preview) ── */
.spa-gl-summary,
.spa-other-gl-summary {
display: flex;
flex-direction: column;
gap: 0.9rem;
margin-top: 1.25rem;
}
.spa-gl-summary__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0 0.5rem;
}
.spa-gl-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.15rem;
border: 1px solid #e3e7f2;
border-radius: 12px;
background: #fff;
h5 {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #91a0bc;
}
}
.spa-gl-row {
display: grid;
grid-template-columns: minmax(220px, 40%) 1fr;
gap: 1rem;
align-items: baseline;
padding: 0.2rem 0;
}
.spa-gl-row__label {
color: #1f2f4a;
font-weight: 500;
}
.spa-gl-row__value {
color: #101828;
font-weight: 600;
word-break: break-word;
}
.spa-other-gl-table {
margin-top: 0.5rem;
margin-bottom: 0.75rem;
table-layout: fixed;
width: 100%;
&:first-of-type {
margin-top: 0.25rem;
}
th.mat-mdc-header-cell,
td.mat-mdc-cell {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
th.mat-mdc-header-cell:first-child,
td.mat-mdc-cell:first-child {
width: 38%;
}
th.mat-mdc-header-cell:last-child,
td.mat-mdc-cell:last-child {
width: 62%;
word-break: break-word;
}
}
.spa-other-gl-summary .mat-elevation-z1 {
border-radius: 10px;
overflow: hidden;
}
@media (width <= 768px) {
.spa-gl-row {
grid-template-columns: 1fr;
gap: 0.35rem;
}
}
@@ -0,0 +1,142 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core';
import { Accounting } from 'app/core/utils/accounting';
import { OptionData } from 'app/shared/models/option-data.model';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-preview-step',
templateUrl: './daily-deposit-scheme-product-preview-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-preview-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
FaIconComponent,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FindPipe,
DateFormatPipe,
FormatNumberPipe,
YesnoPipe
]
})
export class DailyDepositSchemeProductPreviewStepComponent implements OnInit, OnChanges {
private accounting = inject(Accounting);
@Input() DailyDepositSchemeProductsTemplate: any;
@Input() chartSlabsDisplayedColumns: any[] = [];
@Input() accountingRuleData: any;
@Input() DailyDepositSchemeProduct: any;
@Output() submitEvent = new EventEmitter();
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType'
];
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'incomeAccountId'
];
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
accountingRule: OptionData | undefined;
ngOnInit() {
this.setCurrentValues();
}
ngOnChanges(_changes: SimpleChanges): void {
this.setCurrentValues();
}
setCurrentValues(): void {
if (this.DailyDepositSchemeProduct) {
this.accountingRule = this.accounting.getAccountingRuleFrom(this.DailyDepositSchemeProduct.accountingRule);
}
}
isNoneAccounting(): boolean {
return this.accounting.isNoneAccountingRuleId(this.DailyDepositSchemeProduct.accountingRule);
}
isCashOrAccrualAccounting(): boolean {
return this.accounting.isCashOrAccrualAccountingRuleId(this.DailyDepositSchemeProduct.accountingRule);
}
isAccrualAccounting(): boolean {
return this.accounting.isAccrualAccountingRuleId(this.DailyDepositSchemeProduct.accountingRule);
}
getLockinPeriodFrequencyTypeLabel(value: any): string {
if (!value && value !== 0) return '';
const options = this.DailyDepositSchemeProductsTemplate?.lockinPeriodFrequencyTypeOptions || [];
const found = options.find((o: any) => o.id === value);
return found?.value || String(value);
}
getGlAccountName(id: any): string {
if (!id) return '';
const accounts = this.DailyDepositSchemeProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? [];
const found = accounts.find((a: any) => a.id === id || a.id === +id);
return found ? `${found.name} (${found.glCode})` : String(id);
}
formatGlAccountById(glAccountId: number, options: any[]): string {
if (!glAccountId || !Array.isArray(options)) return '-';
const account = this.accounting.glAccountLookUp(glAccountId, options as any);
return account ? `(${account.glCode}) ${account.name}` : '-';
}
formatGlAccountByCode(glCode: string, options: any[]): string {
if (!glCode || !Array.isArray(options)) return glCode || '-';
const account = options.find((o: any) => o?.glCode === glCode);
return account ? `(${account.glCode}) ${account.name}` : glCode;
}
}
@@ -0,0 +1,123 @@
<!--
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/.
-->
<form [formGroup]="DailyDepositSchemeProductSettingsForm">
<!-- Premature option -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-balance-scale"></i></span>
<span>Premature option</span>
</div>
<div class="rdps-body">
<div class="rdps-premature-row">
<mat-checkbox formControlName="loanFacility">Loan Facility</mat-checkbox>
<mat-checkbox formControlName="prematureWithdrawalOption">Premature Withdrawal option</mat-checkbox>
<mat-checkbox formControlName="tenureFlexibility">Tenure Flexibility</mat-checkbox>
<mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
<mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
</div>
</div>
</div>
<!-- Lock-in Period -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-lock"></i></span>
<span>{{ 'labels.inputs.Lock-in Period' | translate }}</span>
</div>
<div class="rdps-body">
<div class="rdps-lockin-row">
<span>Enable Lock-in Period</span>
<mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
</div>
@if (DailyDepositSchemeProductSettingsForm.value.enableLockinPeriod) {
<div class="rdps-lockin-fields">
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>Frequency*</mat-label>
<input type="number" matInput formControlName="lockinPeriodFrequency" min="1" step="1" />
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>Type*</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div>
</div>
<!-- Maturity Instructions -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-lock"></i></span>
<span>Maturity Instructions</span>
</div>
<div class="rdps-body rdps-body--list">
<div class="rdps-list-row">
<span>Maturity Instructions</span>
<mat-checkbox formControlName="maturityInstructions"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span
>Option to credit maturity proceeds directly to the linked account or reinvest in another product (like
FD).</span
>
<mat-checkbox formControlName="creditMaturityProceeds"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Penalty / Grace Period for Missed Installments</span>
<mat-checkbox formControlName="penaltyGracePeriodMissedInstallments"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Panel Request Applicable</span>
<mat-checkbox formControlName="panelRequestApplicable"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>No penalty for nominee / legal heir closure</span>
<mat-checkbox formControlName="noPenaltyNomineeHeirClosure"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Deceased Settlement</span>
<mat-checkbox formControlName="deceasedSettlement"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Allow Debit Txn after due date</span>
<mat-checkbox formControlName="allowDebitTxnAfterDueDate"></mat-checkbox>
</div>
</div>
</div>
<!-- Additional Rules -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-cog"></i></span>
<span>Additional Rules</span>
</div>
<div class="rdps-body">
<div class="rdps-layout rdps-layout--additional">
<div class="rdps-toggle rdps-toggle--inline">
<mat-checkbox formControlName="accountClosedZeroBalanceInDayClosing">
<span>Account Closed with Zero balance in Day Closing</span>
</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>Balance Required for Interest Calculation</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
</form>
@@ -0,0 +1,164 @@
/**
* 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;
.rdps-section {
border: 1px solid $border;
border-radius: 10px;
background: #fff;
& + & {
margin-top: 16px;
}
&__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:not(.rdps-section__badge) {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.rdps-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.rdps-body {
background: #fff;
padding: 18px 24px 24px;
}
.rdps-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.rdps-premature-row {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
}
.rdps-layout {
display: grid;
gap: 16px;
}
.rdps-layout--additional {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.rdps-toggle {
display: flex;
align-items: center;
min-height: 40px;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
&--full {
min-height: auto;
margin-bottom: 16px;
}
}
.rdps-body--list {
padding: 0 24px;
}
.rdps-list-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 0;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
&:last-child {
border-bottom: none;
}
span {
max-width: 85%;
line-height: 1.4;
}
}
.rdps-lockin-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
}
.rdps-lockin-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
&--single {
grid-template-columns: minmax(0, 1fr);
max-width: 50%;
}
@media (width <= 640px) {
grid-template-columns: 1fr;
&--single {
max-width: 100%;
}
}
}
@@ -0,0 +1,95 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-settings-step',
templateUrl: './daily-deposit-scheme-product-settings-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-settings-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox
]
})
export class DailyDepositSchemeProductSettingsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
@Input() DailyDepositSchemeProductsTemplate: any;
DailyDepositSchemeProductSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any;
constructor() {
this.createDailyDepositSchemeProductSettingsForm();
}
ngOnInit() {
this.lockinPeriodFrequencyTypeData = this.DailyDepositSchemeProductsTemplate.lockinPeriodFrequencyTypeOptions;
if (this.DailyDepositSchemeProductsTemplate?.id) {
this.DailyDepositSchemeProductSettingsForm.patchValue({
loanFacility: this.DailyDepositSchemeProductsTemplate.loanFacility ?? false,
prematureWithdrawalOption: this.DailyDepositSchemeProductsTemplate.prematureWithdrawalOption ?? false,
tenureFlexibility: this.DailyDepositSchemeProductsTemplate.tenureFlexibility ?? false,
lienMarked: this.DailyDepositSchemeProductsTemplate.lienMarked ?? false,
withdrawalFeeForTransfers: this.DailyDepositSchemeProductsTemplate.withdrawalFeeForTransfers ?? false,
enableLockinPeriod: this.DailyDepositSchemeProductsTemplate.enableLockinPeriod ?? false,
lockinPeriodFrequency: this.DailyDepositSchemeProductsTemplate.lockinPeriodFrequency ?? '',
lockinPeriodFrequencyType: this.DailyDepositSchemeProductsTemplate.lockinPeriodFrequencyType?.id ?? '',
maturityInstructions: this.DailyDepositSchemeProductsTemplate.maturityInstructions ?? false,
creditMaturityProceeds: this.DailyDepositSchemeProductsTemplate.creditMaturityProceeds ?? false,
penaltyGracePeriodMissedInstallments:
this.DailyDepositSchemeProductsTemplate.penaltyGracePeriodMissedInstallments ?? false,
panelRequestApplicable: this.DailyDepositSchemeProductsTemplate.panelRequestApplicable ?? false,
noPenaltyNomineeHeirClosure: this.DailyDepositSchemeProductsTemplate.noPenaltyNomineeHeirClosure ?? false,
deceasedSettlement: this.DailyDepositSchemeProductsTemplate.deceasedSettlement ?? false,
allowDebitTxnAfterDueDate: this.DailyDepositSchemeProductsTemplate.allowDebitTxnAfterDueDate ?? false,
accountClosedZeroBalanceInDayClosing:
this.DailyDepositSchemeProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false,
minBalanceForInterestCalculation: this.DailyDepositSchemeProductsTemplate.minBalanceForInterestCalculation ?? ''
});
}
}
createDailyDepositSchemeProductSettingsForm() {
this.DailyDepositSchemeProductSettingsForm = this.formBuilder.group({
loanFacility: [false],
prematureWithdrawalOption: [false],
tenureFlexibility: [false],
lienMarked: [false],
withdrawalFeeForTransfers: [false],
enableLockinPeriod: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
maturityInstructions: [false],
creditMaturityProceeds: [false],
penaltyGracePeriodMissedInstallments: [false],
panelRequestApplicable: [false],
noPenaltyNomineeHeirClosure: [false],
deceasedSettlement: [false],
allowDebitTxnAfterDueDate: [false],
accountClosedZeroBalanceInDayClosing: [false],
minBalanceForInterestCalculation: ['']
});
}
get DailyDepositSchemeProductSettings() {
const settings = { ...this.DailyDepositSchemeProductSettingsForm.value };
for (const key in settings) {
if (settings[key] === '') {
delete settings[key];
}
}
return settings;
}
}
@@ -0,0 +1,192 @@
<!--
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/.
-->
<form [formGroup]="DailyDepositSchemeProductTermsForm">
<div class="rdpt-section">
<div class="rdpt-section__header">
<span class="rdpt-section__badge"><i class="fas fa-coins"></i></span>
<span>Deposit Amount</span>
</div>
<div class="rdpt-body">
<div class="rdpt-grid rdpt-grid--three">
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minDepositAmount" min="0" step="0.01" />
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxDepositAmount" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
<div class="rdpt-section">
<div class="rdpt-section__header">
<span class="rdpt-section__badge"><i class="fas fa-chart-line"></i></span>
<span>Interest Rate &amp; Posting Terms</span>
</div>
<div class="rdpt-body">
<div class="rdpt-banner">
<span class="rdpt-banner__icon">ℹ️</span>
<span>
Daily Deposit Scheme base rate:
<b>{{ DailyDepositSchemeProductTermsForm.get('nominalAnnualInterestRate').value }}%</b>
</span>
</div>
<div class="rdpt-grid rdpt-grid--three">
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
<input
type="number"
matInput
matTooltip="{{ 'tooltips.The default interest rate set' | translate }}"
formControlName="nominalAnnualInterestRate"
required
min="0"
step="0.01"
/>
@if (DailyDepositSchemeProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) {
<mat-error>
{{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (DailyDepositSchemeProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) {
<mat-error>{{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Effective Date</mat-label>
<input matInput [matDatepicker]="effectiveDatePicker" formControlName="effectiveDate" />
<mat-datepicker-toggle matSuffix [for]="effectiveDatePicker"></mat-datepicker-toggle>
<mat-datepicker #effectiveDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select
formControlName="interestCalculateSelector"
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
>
@for (opt of interestCalculationTypeOptionData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="rdpt-grid rdpt-grid--three">
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Interest Calculate</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (mode of interestCalculationTypeData; track mode.id) {
<mat-option [value]="mode.id">{{ mode.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Interest Compounding Period</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (opt of interestCompoundingPeriodTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Interest Compounding Period <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Days in Year</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required>
@for (opt of interestCalculationDaysInYearTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Days in Year <strong>is required</strong></mat-error>
</mat-form-field>
@if (DailyDepositSchemeProductTermsForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod">
@for (period of periodFrequencyData; track period.id) {
<mat-option [value]="period.value">{{ period.value }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select
formControlName="interestPostingPeriodType"
matTooltip="{{ 'tooltips.Interest rate is posted or credited to a saving account' | translate }}"
required
>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="rdpt-footer">
<div class="rdpt-footer__grid">
<div class="rdpt-toggle-card">
<div>
<h4>Auto Interest Post</h4>
</div>
<div class="rdpt-toggle-card__choices">
@for (option of yesNoOptions; track option.value) {
<label class="inline-flex items-center cursor-pointer">
<input type="radio" class="w-4 h-4" formControlName="autoPost" [value]="option.value" />
<span class="ml-2 text-sm">{{ option.label }}</span>
</label>
}
</div>
</div>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Frequency (Fix Day)</mat-label>
<input type="number" matInput formControlName="frequencyFixDay" />
<mat-hint>Days</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Last Interest Date</mat-label>
<input matInput [matDatepicker]="lastInterestDatePicker" formControlName="lastInterestDate" />
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
</div>
@if (DailyDepositSchemeProductTermsForm.value.interestCalculationType === 1) {
<div class="rdpt-note">
<span>⚠️</span>
<span
>Daily product-based interest accrual enabled - interest calculated EOD on daily closing balance.</span
>
</div>
}
</div>
</div>
</div>
</form>
@@ -0,0 +1,151 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdpt-section {
border: 1px solid $border;
border-radius: 10px;
background: #fff;
& + & {
margin-top: 16px;
}
&__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;
}
h3,
span:not(.rdpt-section__badge) {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.rdpt-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.rdpt-body {
background: #fff;
}
.rdpt-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
&--three {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
&--three {
grid-template-columns: 1fr;
}
}
}
.rdpt-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.rdpt-banner {
display: flex;
align-items: center;
gap: 10px;
margin: 16px 24px 0;
padding: 12px 14px;
background: #eff6ff;
border: 1px solid #dbeafe;
border-radius: 8px;
color: #1d4ed8;
font-size: 13px;
&__icon {
font-size: 14px;
}
}
.rdpt-footer {
padding: 0 24px 24px;
border-top: 1px solid #f1f5f9;
}
.rdpt-footer__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding-top: 18px;
@media (width <= 900px) {
grid-template-columns: 1fr;
}
}
.rdpt-toggle-card {
padding: 12px 0;
h4 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
&__choices {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
}
.rdpt-note {
display: flex;
align-items: center;
gap: 10px;
margin-top: 18px;
padding: 12px 14px;
border: 1px solid #fde68a;
background: #fffbeb;
color: #b45309;
border-radius: 8px;
font-size: 12px;
}
@@ -0,0 +1,169 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-product-terms-step',
templateUrl: './daily-deposit-scheme-product-terms-step.component.html',
styleUrls: ['./daily-deposit-scheme-product-terms-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip
]
})
export class DailyDepositSchemeProductTermsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() DailyDepositSchemeProductsTemplate: any;
DailyDepositSchemeProductTermsForm: UntypedFormGroup;
interestCompoundingPeriodTypeData: any;
interestPostingPeriodTypeData: any;
interestCalculationTypeData: any;
interestCalculationTypeOptionData: any;
interestCalculationDaysInYearTypeData: any;
periodFrequencyData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() {
this.createDailyDepositSchemeProductTermsForm();
}
ngOnInit() {
this.interestCompoundingPeriodTypeData =
this.DailyDepositSchemeProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.DailyDepositSchemeProductsTemplate.interestPostingPeriodTypeOptions;
// Standard enum (1=Daily, 2=Average) → "Interest Calculate" dropdown → interestCalculationType form control
this.interestCalculationTypeData = this.DailyDepositSchemeProductsTemplate.interestCalculationTypeOptions;
// CBS code values → "Interest Calculation Type" dropdown → interestCalculateSelector form control
this.interestCalculationTypeOptionData =
this.DailyDepositSchemeProductsTemplate.interestCalculationTypeOption || [];
this.interestCalculationDaysInYearTypeData =
this.DailyDepositSchemeProductsTemplate.interestCalculationDaysInYearTypeOptions;
this.periodFrequencyData = this.DailyDepositSchemeProductsTemplate.periodFrequencyOptions || [];
this.setYesNoOptions(this.DailyDepositSchemeProductsTemplate?.yesNoOptions);
const isEditing = !!this.DailyDepositSchemeProductsTemplate.id;
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
const savedEnumTypeId =
this.DailyDepositSchemeProductsTemplate.interestCalculationType?.id ||
this.interestCalculationTypeData?.[0]?.id ||
1;
const savedSelectorId = isEditing
? (this.DailyDepositSchemeProductsTemplate.interestCalculateSelector ??
this.interestCalculationTypeOptionData?.[0]?.id ??
null)
: (this.interestCalculationTypeOptionData?.[0]?.id ?? null);
this.DailyDepositSchemeProductTermsForm.patchValue({
nominalAnnualInterestRate: this.DailyDepositSchemeProductsTemplate.nominalAnnualInterestRate ?? 0,
interestCompoundingPeriodType: this.DailyDepositSchemeProductsTemplate.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.DailyDepositSchemeProductsTemplate.interestPostingPeriodType?.id,
interestCalculationType: savedEnumTypeId,
interestCalculationDaysInYearType: this.DailyDepositSchemeProductsTemplate.interestCalculationDaysInYearType?.id,
effectiveDate: this.DailyDepositSchemeProductsTemplate.effectiveDate
? this.parseDate(this.DailyDepositSchemeProductsTemplate.effectiveDate)
: '',
interestCalculateSelector: savedSelectorId,
averagePeriod: isEditing ? this.DailyDepositSchemeProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod,
autoPost: isEditing ? (this.DailyDepositSchemeProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? (this.DailyDepositSchemeProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.DailyDepositSchemeProductsTemplate.lastInterestDate
? this.parseDate(this.DailyDepositSchemeProductsTemplate.lastInterestDate)
: '',
minDepositAmount: this.DailyDepositSchemeProductsTemplate.minDepositAmount ?? '',
maxDepositAmount: this.DailyDepositSchemeProductsTemplate.maxDepositAmount ?? ''
});
}
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) {
return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
}
if (dateValue instanceof Date) return dateValue;
return new Date(dateValue);
}
private setYesNoOptions(options: any[]) {
if (!Array.isArray(options)) return;
const mapped = options
.map((opt: any) => {
const raw = opt?.name ?? opt?.value ?? opt;
const norm = typeof raw === 'string' ? raw.trim().toUpperCase() : raw;
if (norm === 'YES') return { value: true, label: 'Yes' };
if (norm === 'NO') return { value: false, label: 'No' };
return null;
})
.filter(Boolean);
if (mapped.length) this.yesNoOptions = mapped;
}
createDailyDepositSchemeProductTermsForm() {
this.DailyDepositSchemeProductTermsForm = this.formBuilder.group({
nominalAnnualInterestRate: [
'',
[
Validators.required,
Validators.min(0)
]
],
interestCompoundingPeriodType: [
'',
Validators.required
],
interestPostingPeriodType: [
'',
Validators.required
],
interestCalculationType: [
'',
Validators.required
],
interestCalculationDaysInYearType: [
'',
Validators.required
],
effectiveDate: [''],
interestCalculateSelector: [1],
averagePeriod: ['Monthly'],
autoPost: [true],
frequencyFixDay: [
30,
[Validators.min(0)]
],
lastInterestDate: [''],
minDepositAmount: [''],
maxDepositAmount: ['']
});
}
get DailyDepositSchemeProductTerms() {
const formValue = { ...this.DailyDepositSchemeProductTermsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveDate instanceof Date) {
formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || '';
}
formValue.minDepositAmount = formValue.minDepositAmount;
formValue.maxDepositAmount = formValue.maxDepositAmount;
return formValue;
}
}
@@ -0,0 +1,37 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../products.service';
/**
* Daily Deposit Scheme Product data resolver.
*
* Reuses the Recurring Deposit product read API — a Daily Deposit Scheme
* product is a Recurring Deposit product under the hood.
*/
@Injectable()
export class DailyDepositSchemeProductResolver {
private productsService = inject(ProductsService);
/**
* Returns the Daily Deposit Scheme product data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const productId = route.parent.paramMap.get('productId');
return this.productsService.getRecurringDepositProduct(productId);
}
}
@@ -0,0 +1,172 @@
<!--
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="pl-root">
<div class="pl-header">
<div class="pl-header-left">
<div class="pl-header-ic"><fa-icon icon="calendar-day"></fa-icon></div>
<div class="pl-header-text">
<h2>{{ 'labels.heading.Daily Deposit Scheme Products' | translate }}</h2>
<p>Manage Daily Deposit Scheme products — daily instalment amount, interest rates and charges.</p>
</div>
</div>
<div class="pl-header-right">
<div class="pl-count-chip" matTooltip="Total configured Daily Deposit Scheme products">
<fa-icon icon="layer-group"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITPRODUCT'"
matTooltip="{{ 'tooltips.Create Daily Deposit Scheme Product' | translate }}"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Daily Deposit Scheme Product' | translate }}</span>
</button>
</div>
</div>
<div class="pl-kpis">
<div class="pl-kpi pl-kpi--blue">
<div class="pl-kpi-ic"><fa-icon icon="calendar-day"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalCount | number }}</div>
<div class="pl-kpi-l">Total DDS Products</div>
</div>
</div>
<div class="pl-kpi pl-kpi--teal">
<div class="pl-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ currencyCount | number }}</div>
<div class="pl-kpi-l">{{ currencyCount === 1 ? 'Currency' : 'Currencies' }} in use</div>
</div>
</div>
<div class="pl-kpi pl-kpi--purple">
<div class="pl-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ filteredCount | number }}</div>
<div class="pl-kpi-l">Showing after filter</div>
</div>
</div>
</div>
<div class="pl-card">
<div class="pl-toolbar">
<div class="pl-search">
<fa-icon icon="search" class="pl-search-ic"></fa-icon>
<input
#filter
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
@if (totalCount === 0) {
<div class="pl-empty">
<div class="pl-empty-ic"><fa-icon icon="calendar-day"></fa-icon></div>
<h3>No Daily Deposit Scheme products yet</h3>
<p>Create your first Daily Deposit Scheme product to enable daily-instalment onboarding.</p>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Daily Deposit Scheme Product' | translate }}</span>
</button>
</div>
} @else {
<div class="pl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="pl-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="pl-product-cell">
<div class="pl-product-icon"><fa-icon icon="calendar-day"></fa-icon></div>
<div class="pl-product-meta">
<div class="pl-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="pl-product-sub">{{ p.currency.code }}</div>
}
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<span class="pl-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th mat-header-cell *matHeaderCellDef>Currency</th>
<td mat-cell *matCellDef="let p">
@if (p.currency?.code) {
<span class="pl-currency-pill">{{ p.currency.code }}</span>
} @else {
<span class="muted">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="pl-action-btn pl-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
>
<fa-icon icon="eye"></fa-icon><span>View</span>
</button>
<button
type="button"
class="pl-action-btn pl-action-btn--edit"
[class.pl-action-btn--disabled]="p.isPendingApproval"
[disabled]="p.isPendingApproval"
[routerLink]="p.isPendingApproval ? null : [p.id, 'edit']"
(click)="$event.stopPropagation()"
[matTooltip]="
p.isPendingApproval
? 'This product is locked because an edit is pending approval'
: 'Edit this product'
"
*mifosxHasPermission="'UPDATE_RECURRINGDEPOSITPRODUCT'"
>
<fa-icon [icon]="p.isPendingApproval ? 'lock' : 'edit'"></fa-icon>
<span>{{ p.isPendingApproval ? 'Locked' : 'Edit' }}</span>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="pl-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="pl-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
@@ -0,0 +1,17 @@
/**
* 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 'assets/styles/product-list' as pl;
@include pl.product-list-styles;
/* Daily Deposit Scheme accent */
:host {
--pl-accent: #0f766e;
--pl-accent-l: #ccfbf1;
}
@@ -0,0 +1,124 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader } from '@angular/material/sort';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { MatTooltip } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Daily Deposit Scheme Products component.
*
* Lists Daily Deposit Scheme products. These are Recurring Deposit products
* under the hood (same create/read API) so this list reuses that data source.
*/
@Component({
selector: 'mifosx-daily-deposit-scheme-products',
templateUrl: './daily-deposit-scheme-products.component.html',
styleUrls: ['./daily-deposit-scheme-products.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
FaIconComponent,
MatTable,
MatSort,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatSortHeader,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator
]
})
export class DailyDepositSchemeProductsComponent implements OnInit {
private route = inject(ActivatedRoute);
/** Data table data. */
productData: any[] = [];
/** Columns to be displayed in the Daily Deposit Scheme products table. */
displayedColumns: string[] = [
'name',
'shortName',
'currency',
'actions'
];
/** Data source for the table. */
dataSource: MatTableDataSource<any>;
filteredCount = 0;
get totalCount(): number {
return this.productData?.length || 0;
}
get currencyCount(): number {
const set = new Set<string>();
for (const p of this.productData || []) {
if (p?.currency?.code) set.add(p.currency.code);
}
return set.size;
}
/** Paginator for the table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for the table. */
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor() {
this.route.data.subscribe((data: { dailyDepositSchemeProducts: any }) => {
this.productData = (data.dailyDepositSchemeProducts || []).filter(
(product: any) => product.productCategory === 'DDS'
);
});
}
ngOnInit() {
this.setProducts();
}
/**
* Filters data in the table based on the passed value.
* @param {string} filterValue Value to filter data.
*/
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
/**
* Initializes the data source, paginator and sorter for the table.
*/
setProducts() {
this.dataSource = new MatTableDataSource(this.productData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
}
@@ -0,0 +1,35 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../products.service';
/**
* Daily Deposit Scheme Products data resolver.
*
* A Daily Deposit Scheme product is a Recurring Deposit product under the hood,
* so this reuses the same read API as Recurring Deposit Products.
*/
@Injectable()
export class DailyDepositSchemeProductsResolver {
private productsService = inject(ProductsService);
/**
* Returns the Daily Deposit Scheme products data.
* @returns {Observable<any>}
*/
resolve(): Observable<any> {
return this.productsService.getRecurringDepositProducts();
}
}
@@ -0,0 +1,35 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../../products.service';
/**
* Recurring Deposits Account Template resolver.
*/
@Injectable()
export class DailyDepositSchemeProductAndTemplateResolver {
private productsService = inject(ProductsService);
/**
* Returns the Recurring Deposits Product and Template.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const productId = route.parent.paramMap.get('productId');
return this.productsService.getRecurringDepositProductAndTemplate(productId);
}
}
@@ -0,0 +1,202 @@
<!--
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="rdp-container">
<div class="rdp-header">
<i class="fas fa-sync-alt rdp-header__icon"></i>
<h2 class="rdp-header__title">{{ 'labels.heading.Edit Daily Deposit Scheme Product' | translate }}</h2>
</div>
<mat-tab-group
class="rdp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-info-circle rdp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-details-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
></mifosx-daily-deposit-scheme-product-details-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-coins rdp-tab-icon"></i>
{{ 'labels.inputs.CURRENCY' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-currency-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
></mifosx-daily-deposit-scheme-product-currency-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-sliders-h rdp-tab-icon"></i>
{{ 'labels.inputs.TERMS' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-terms-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
></mifosx-daily-deposit-scheme-product-terms-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-cog rdp-tab-icon"></i>
{{ 'labels.inputs.SETTINGS' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-settings-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
></mifosx-daily-deposit-scheme-product-settings-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-chart-area rdp-tab-icon"></i>
CBS Base Chart
</ng-template>
<div class="rdp-tab-content">
@if (activeBaseVersion) {
<div class="base-rate-card">
<div class="base-rate-header">
<div>
<h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
<p class="base-rate-meta">
Active CBS base chart
<span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</p>
</div>
<span class="base-rate-badge">Active</span>
</div>
@if (activeBaseVersion.remarks) {
<p class="base-rate-remarks">{{ activeBaseVersion.remarks }}</p>
}
<table class="base-rate-table" mat-table [dataSource]="activeBaseVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? 'N/A' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="rdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: rdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-receipt rdp-tab-icon"></i>
{{ 'labels.inputs.CHARGES' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-charges-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
[currencyCode]="DailyDepositSchemeProductCurrencyForm.get('currencyCode')"
></mifosx-daily-deposit-scheme-product-charges-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-book rdp-tab-icon"></i>
{{ 'labels.inputs.ACCOUNTING' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-accounting-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
[accountingRuleData]="accountingRuleData"
[DailyDepositSchemeProductFormValid]="DailyDepositSchemeProductFormValid"
[isDormancyTrackingActive]="DailyDepositSchemeProductCurrencyForm.get('dormancyAlert')"
>
</mifosx-daily-deposit-scheme-product-accounting-step>
</div>
</mat-tab>
@if (DailyDepositSchemeProductFormValid) {
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-eye rdp-tab-icon"></i>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-daily-deposit-scheme-product-preview-step
[DailyDepositSchemeProductsTemplate]="DailyDepositSchemeProductsTemplate"
[chartSlabsDisplayedColumns]="rdSlabColumns"
[accountingRuleData]="accountingRuleData"
[DailyDepositSchemeProduct]="DailyDepositSchemeProductPreview"
(submitEvent)="submit()"
></mifosx-daily-deposit-scheme-product-preview-step>
</div>
</mat-tab>
}
</mat-tab-group>
<div class="rdp-action-bar">
<button mat-raised-button [routerLink]="['../../']" class="rdp-action-bar__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<div class="rdp-action-bar__right">
@if (selectedTabIndex > 0) {
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1">
<i class="fas fa-arrow-left" style="margin-right: 8px"></i>
{{ 'labels.buttons.Previous' | translate }}
</button>
}
@if (selectedTabIndex < totalTabs - 1) {
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
{{ 'labels.buttons.Next' | translate }}
<i class="fas fa-arrow-right" style="margin-left: 8px"></i>
</button>
}
@if (DailyDepositSchemeProductFormValid && selectedTabIndex === totalTabs - 1) {
<button mat-raised-button color="primary" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }}
</button>
}
</div>
</div>
</div>
@@ -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/.
*/
$primary: #1353a0;
.rdp-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.rdp-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
&__icon {
font-size: 20px;
color: $primary;
}
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.rdp-tabs {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
::ng-deep {
.mat-mdc-tab-header {
border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.mat-mdc-tab {
min-width: 100px;
padding: 0 12px;
height: 52px;
}
.mat-mdc-tab-body-wrapper {
min-height: 200px;
}
.mat-mdc-tab-body {
overflow: visible !important;
}
.mat-mdc-tab-body-content {
overflow: visible !important;
}
}
}
.rdp-tab-icon {
margin-right: 8px;
font-size: 14px;
}
.rdp-tab-content {
padding: 24px 16px;
}
.base-rate-card {
padding: 16px;
border: 1px solid #dbe4f0;
border-radius: 14px;
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.base-rate-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.base-rate-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.base-rate-meta {
margin: 6px 0 0;
color: #5b6b7f;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.base-rate-badge {
padding: 4px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.base-rate-remarks {
margin: 0 0 12px;
color: #334155;
}
.base-rate-table {
width: 100%;
border-collapse: collapse;
}
.rdp-action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel {
color: rgb(0 0 0 / 60%);
}
&__right {
display: flex;
gap: 12px;
}
}
@@ -0,0 +1,305 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
/** Custom Components */
import { DailyDepositSchemeProductDetailsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component';
import { DailyDepositSchemeProductCurrencyStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component';
import { DailyDepositSchemeProductTermsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component';
import { DailyDepositSchemeProductSettingsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component';
import { DailyDepositSchemeProductChargesStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component';
import { DailyDepositSchemeProductAccountingStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { Dates } from 'app/core/utils/dates';
import { MatTabsModule } from '@angular/material/tabs';
import { DailyDepositSchemeProductPreviewStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-edit-daily-deposit-scheme-product',
templateUrl: './edit-daily-deposit-scheme-product.component.html',
styleUrls: ['./edit-daily-deposit-scheme-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTabsModule,
DailyDepositSchemeProductDetailsStepComponent,
DailyDepositSchemeProductCurrencyStepComponent,
DailyDepositSchemeProductTermsStepComponent,
DailyDepositSchemeProductSettingsStepComponent,
DailyDepositSchemeProductChargesStepComponent,
DailyDepositSchemeProductAccountingStepComponent,
DailyDepositSchemeProductPreviewStepComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
]
})
export class EditDailyDepositSchemeProductComponent {
private route = inject(ActivatedRoute);
private productsService = inject(ProductsService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(DailyDepositSchemeProductDetailsStepComponent, { static: true })
DailyDepositSchemeProductDetailsStep: DailyDepositSchemeProductDetailsStepComponent;
@ViewChild(DailyDepositSchemeProductCurrencyStepComponent, { static: true })
DailyDepositSchemeProductCurrencyStep: DailyDepositSchemeProductCurrencyStepComponent;
@ViewChild(DailyDepositSchemeProductTermsStepComponent, { static: true })
DailyDepositSchemeProductTermsStep: DailyDepositSchemeProductTermsStepComponent;
@ViewChild(DailyDepositSchemeProductSettingsStepComponent, { static: true })
DailyDepositSchemeProductSettingsStep: DailyDepositSchemeProductSettingsStepComponent;
@ViewChild(DailyDepositSchemeProductChargesStepComponent, { static: true })
DailyDepositSchemeProductChargesStep: DailyDepositSchemeProductChargesStepComponent;
@ViewChild(DailyDepositSchemeProductAccountingStepComponent, { static: true })
DailyDepositSchemeProductAccountingStep: DailyDepositSchemeProductAccountingStepComponent;
DailyDepositSchemeProductsTemplate: any;
accountingRuleData: string[] = [];
rdInterestRates: any[] = [];
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
selectedTabIndex = 0;
/**
* @param {ActivatedRoute} route Activated Route.
* @param {ProductsService} productsService Products Service.
* @param {Router} router Router for navigation.
* @param {SettingsService} settingsService Settings Service.
*/
constructor() {
this.route.data.subscribe((data: { DailyDepositSchemeProductAndTemplate: any }) => {
this.DailyDepositSchemeProductsTemplate = data.DailyDepositSchemeProductAndTemplate;
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
}
get DailyDepositSchemeProductDetailsForm() {
return this.DailyDepositSchemeProductDetailsStep?.DailyDepositSchemeProductDetailsForm;
}
get DailyDepositSchemeProductCurrencyForm() {
return this.DailyDepositSchemeProductCurrencyStep?.DailyDepositSchemeProductCurrencyForm;
}
get DailyDepositSchemeProductTermsForm() {
return this.DailyDepositSchemeProductTermsStep?.DailyDepositSchemeProductTermsForm;
}
get DailyDepositSchemeProductSettingsForm() {
return this.DailyDepositSchemeProductSettingsStep?.DailyDepositSchemeProductSettingsForm;
}
get DailyDepositSchemeProductAccountingForm() {
return this.DailyDepositSchemeProductAccountingStep?.DailyDepositSchemeProductAccountingForm;
}
get totalTabs(): number {
return this.DailyDepositSchemeProductFormValid ? 8 : 7;
}
get DailyDepositSchemeProductFormValid() {
return !!this.DailyDepositSchemeProductDetailsForm?.valid;
}
get activeBaseVersion() {
const chart =
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE' && version.isActive) ??
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE') ??
this.rdInterestRates[0];
if (chart) {
return {
...chart,
versionLabel: chart.versionLabel?.replace('FD', 'RD'),
remarks: chart.remarks?.replace('FD', 'RD')
};
}
return null;
}
get activeBaseSlabs() {
return this.activeBaseVersion?.slabs || [];
}
get DailyDepositSchemeProduct() {
// accountTypeGroup / autoInterestPost / autoInterestPostWhen are the legacy Account Type
// Creation screen's own fields (added for the simplified Create form) - not real product API
// parameters, so they must not leak into the update payload the way a plain spread would.
// accountTypeGroup is deliberately NOT re-folded into description here (unlike Create): it
// was already folded in once at creation time, and this getter re-runs on every change
// detection cycle while editing, so re-prepending it here would keep stacking duplicate
// "Group - " prefixes onto the same description. autoInterestPost/When aren't re-synced onto
// the Terms tab either, since Edit's own Terms/Settings/Accounting tabs remain fully visible
// and directly editable.
const { accountTypeGroup, autoInterestPost, autoInterestPostWhen, autoInterestPostDay, ...detailsPayload } =
this.DailyDepositSchemeProductDetailsStep.DailyDepositSchemeProductDetails;
return {
...detailsPayload,
...this.DailyDepositSchemeProductCurrencyStep.DailyDepositSchemeProductCurrency,
...this.DailyDepositSchemeProductTermsStep.DailyDepositSchemeProductTerms,
...this.DailyDepositSchemeProductSettingsStep.DailyDepositSchemeProductSettings,
...this.DailyDepositSchemeProductChargesStep.DailyDepositSchemeProductCharges,
...this.DailyDepositSchemeProductAccountingStep.DailyDepositSchemeProductAccounting,
// charts intentionally omitted here - the backend no longer requires a charts payload for RD
// updates once rdSlabs is present, and this getter's chart data comes from the shared CBS
// base-rate template (not this product's own chart), which caused spurious
// "chart.overlapping.from.and.end.dates" errors when a product's real chart had different
// dates. Still included in DailyDepositSchemeProductPreview below for the read-only display tab.
rdSlabs: this.activeBaseSlabs
};
}
get DailyDepositSchemeProductPreview() {
return {
...this.DailyDepositSchemeProduct,
charts: this.DailyDepositSchemeProductCharts,
rdSlabs: this.activeBaseSlabs
};
}
get DailyDepositSchemeProductCharts() {
const activeBaseVersion = this.activeBaseVersion;
if (!activeBaseVersion?.slabs?.length) {
return [];
}
const dateFormat = this.settingsService.dateFormat;
return [
{
name: activeBaseVersion.versionLabel || 'CBS Base Chart',
description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart',
fromDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) ||
activeBaseVersion.effectiveFromDate,
...(activeBaseVersion.effectiveToDate
? {
endDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) ||
activeBaseVersion.effectiveToDate
}
: {}),
isPrimaryGroupingByAmount: false,
locale: this.settingsService.language.code,
dateFormat,
chartSlabs: activeBaseVersion.slabs.map((slab: any) => {
const chartSlab: any = {
periodType: slab.periodTypeEnum,
fromPeriod: slab.fromPeriod,
toPeriod: slab.toPeriod,
annualInterestRate: slab.annualInterestRate,
description: slab.description || '',
incentives: [] as any[]
};
if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') {
chartSlab.amountRangeFrom = slab.amountRangeFrom;
}
if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') {
chartSlab.amountRangeTo = slab.amountRangeTo;
}
return chartSlab;
})
}
] as any[];
}
submit() {
const DailyDepositSchemeProduct: any = this.pruneEmptyValues({
...this.DailyDepositSchemeProduct,
charges: (this.DailyDepositSchemeProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code,
dateFormat: this.settingsService.dateFormat
});
if (!DailyDepositSchemeProduct.description) {
DailyDepositSchemeProduct.description = '';
}
delete DailyDepositSchemeProduct.advancedAccountingRules;
const productId = this.DailyDepositSchemeProductsTemplate.id;
this.productsService
.updateRecurringDepositProduct(productId, DailyDepositSchemeProduct)
.subscribe((response: any) => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
private pruneEmptyValues<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T;
}
if (value && typeof value === 'object') {
return Object.entries(value as Record<string, any>).reduce(
(acc, [
key,
currentValue
]) => {
const cleanedValue = this.pruneEmptyValues(currentValue);
if (cleanedValue === undefined) {
return acc;
}
return {
...acc,
[key]: cleanedValue
};
},
{} as Record<string, any>
) as T;
}
if (value === '' || value === null || value === undefined) {
return undefined as T;
}
return value;
}
}
@@ -0,0 +1,17 @@
<!--
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="tab-container mat-typography">
<mifosx-entity-datatable-tab
entityType="Daily Deposit Scheme Product"
[entityId]="entityId"
[multiRowDatatableFlag]="multiRowDatatableFlag"
[entityDatatable]="entityDatatable"
>
</mifosx-entity-datatable-tab>
</div>
@@ -0,0 +1,7 @@
/**
* 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/.
*/
@@ -0,0 +1,38 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EntityDatatableTabComponent } from '../../../../shared/tabs/entity-datatable-tab/entity-datatable-tab.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-datatable-tab',
templateUrl: './daily-deposit-scheme-datatable-tab.component.html',
styleUrls: ['./daily-deposit-scheme-datatable-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
EntityDatatableTabComponent
]
})
export class DailyDepositSchemeDatatableTabComponent {
private route = inject(ActivatedRoute);
entityId: string;
entityDatatable: any;
multiRowDatatableFlag: boolean;
constructor() {
this.entityId = this.route.parent.parent.snapshot.paramMap.get('productId');
this.route.data.subscribe((data: { recurringDepositDatatable: any }) => {
this.entityDatatable = data.recurringDepositDatatable;
this.multiRowDatatableFlag = this.entityDatatable.columnHeaders[0].columnName === 'id' ? true : false;
});
}
}
@@ -0,0 +1,843 @@
<!--
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="vsp-root">
<!-- HERO -->
<div class="vsp-hero">
<div class="vsp-hero-left">
<div class="vsp-hero-icon"><fa-icon icon="calendar-check"></fa-icon></div>
<div class="vsp-hero-text">
<div class="vsp-hero-eyebrow">Daily Deposit Scheme Product</div>
<h2 class="vsp-hero-title">{{ DailyDepositSchemeProduct.name }}</h2>
<div class="vsp-hero-meta">
<span class="vsp-pill vsp-pill--code">{{ DailyDepositSchemeProduct.shortName }}</span>
@if (DailyDepositSchemeProduct.currency?.code) {
<span class="vsp-pill vsp-pill--teal">{{ DailyDepositSchemeProduct.currency.code }}</span>
}
<span
class="vsp-pill"
[class.vsp-pill--green]="!DailyDepositSchemeProduct.endDate"
[class.vsp-pill--amber]="DailyDepositSchemeProduct.endDate"
>
<fa-icon [icon]="DailyDepositSchemeProduct.endDate ? 'times-circle' : 'check-circle'"></fa-icon>
{{ DailyDepositSchemeProduct.endDate ? 'Inactive' : 'Active' }}
</span>
</div>
@if (DailyDepositSchemeProduct.description) {
<p class="vsp-hero-desc">{{ DailyDepositSchemeProduct.description }}</p>
}
</div>
</div>
<button
class="vsp-edit-btn"
type="button"
[routerLink]="['../edit']"
*mifosxHasPermission="'UPDATE_DailyDepositSchemeProduct'"
>
<fa-icon icon="edit"></fa-icon>
<span>{{ 'labels.buttons.Edit' | translate }}</span>
</button>
</div>
<!-- DETAILS -->
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--blue"><fa-icon icon="info-circle"></fa-icon></div>
<h3>{{ 'labels.inputs.Details' | translate }}</h3>
</div>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Short Name' | translate }}</span>
<span class="vsp-v"
><strong>{{ DailyDepositSchemeProduct.shortName }}</strong></span
>
</div>
@if (DailyDepositSchemeProduct.description) {
<div class="vsp-kv vsp-kv--span">
<span class="vsp-k">{{ 'labels.inputs.Description' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.description }}</span>
</div>
}
</div>
</section>
<!-- PRODUCT CLASSIFICATION -->
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--purple"><fa-icon icon="layer-group"></fa-icon></div>
<h3>Product Classification</h3>
</div>
<div class="vsp-grid">
@if (DailyDepositSchemeProduct.accountType) {
<div class="vsp-kv">
<span class="vsp-k">Account Type</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.accountType }}</span>
</div>
}
@if (DailyDepositSchemeProduct.customerSegment) {
<div class="vsp-kv">
<span class="vsp-k">Customer Segment</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.customerSegment }}</span>
</div>
}
@if (DailyDepositSchemeProduct.glAccountId) {
<div class="vsp-kv">
<span class="vsp-k">GL Account (Liability)</span>
<span class="vsp-v">{{
DailyDepositSchemeProduct.glAccountName || DailyDepositSchemeProduct.glAccountId
}}</span>
</div>
}
@if (DailyDepositSchemeProduct.effectiveTo) {
<div class="vsp-kv">
<span class="vsp-k">Effective To</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.effectiveTo | dateFormat }}</span>
</div>
}
@if (DailyDepositSchemeProduct.endDate) {
<div class="vsp-kv">
<span class="vsp-k">End Date</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.endDate | dateFormat }}</span>
</div>
}
<div class="vsp-kv">
<span class="vsp-k">Introducer Required</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.introducerRequired"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.introducerRequired"
>
{{ DailyDepositSchemeProduct.introducerRequired | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Deposit Account Link</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.shareLinkingEnabled"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.shareLinkingEnabled"
>
{{ DailyDepositSchemeProduct.shareLinkingEnabled | yesNo }}
</span>
</span>
</div>
</div>
</section>
<!-- CURRENCY -->
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--teal"><fa-icon icon="coins"></fa-icon></div>
<h3>{{ 'labels.inputs.Currency' | translate }}</h3>
</div>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="vsp-v"
><strong
>({{ DailyDepositSchemeProduct.currency.code }}) {{ DailyDepositSchemeProduct.currency.name }}</strong
></span
>
</div>
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.currency.decimalPlaces }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Dormancy Alert</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.dormancyAlert | yesNo }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Balance Swap Option</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.balanceSwapOption | yesNo }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">MSME</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.msme | yesNo }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Nomination Facility</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.nominationFacility | yesNo }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Auto Debit Facility</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.autoDebitFacility | yesNo }}</span>
</div>
</div>
</section>
<!-- TERMS -->
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--amber"><fa-icon icon="percentage"></fa-icon></div>
<h3>{{ 'labels.inputs.Terms' | translate }}</h3>
</div>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Deposit Amount' | translate }}</span>
<span class="vsp-v vsp-v--accent">
{{ DailyDepositSchemeProduct.depositAmount | formatNumber }}
<span class="vsp-suffix"
>(Min: {{ DailyDepositSchemeProduct.minDepositAmount || DailyDepositSchemeProduct.depositAmount }}, Max:
{{ DailyDepositSchemeProduct.maxDepositAmount || DailyDepositSchemeProduct.depositAmount }})</span
>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Nominal Annual Interest' | translate }}</span>
<span class="vsp-v vsp-v--accent"
>{{ DailyDepositSchemeProduct.nominalAnnualInterestRate | formatNumber
}}<span class="vsp-suffix">%</span></span
>
</div>
@if (DailyDepositSchemeProduct.effectiveDate) {
<div class="vsp-kv">
<span class="vsp-k">Effective Date</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.effectiveDate | dateFormat }}</span>
</div>
}
@if (DailyDepositSchemeProduct.interestCalculationTypeName) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Interest Calculation Type' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.interestCalculationTypeName }}</span>
</div>
}
<div class="vsp-kv">
<span class="vsp-k">Interest Calculate</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.interestCalculationType?.value }}</span>
</div>
@if (DailyDepositSchemeProduct.averagePeriod) {
<div class="vsp-kv">
<span class="vsp-k">Average Period</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.averagePeriod }}</span>
</div>
}
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Interest Posting Period' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.interestPostingPeriodType.value }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Interest Compounding Period' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.interestCompoundingPeriodType.value }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Days in Year' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.interestCalculationDaysInYearType.value }}</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Auto Post</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.autoPost"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.autoPost"
>
{{ DailyDepositSchemeProduct.autoPost | yesNo }}
</span>
</span>
</div>
@if (DailyDepositSchemeProduct.frequencyFixDay) {
<div class="vsp-kv">
<span class="vsp-k">Frequency (Fix Day)</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.frequencyFixDay }}</span>
</div>
}
@if (DailyDepositSchemeProduct.lastInterestDate) {
<div class="vsp-kv">
<span class="vsp-k">Last Interest Date</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.lastInterestDate | dateFormat }}</span>
</div>
}
</div>
</section>
<!-- SETTINGS -->
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--indigo"><fa-icon icon="sliders-h"></fa-icon></div>
<h3>{{ 'labels.inputs.Settings' | translate }}</h3>
</div>
<h4 class="vsp-sub-h">Premature Option</h4>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">Loan Facility</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.loanFacility"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.loanFacility"
>
{{ DailyDepositSchemeProduct.loanFacility | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Premature Withdrawal Option</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.prematureWithdrawalOption"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.prematureWithdrawalOption"
>
{{ DailyDepositSchemeProduct.prematureWithdrawalOption | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Tenure Flexibility</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.tenureFlexibility"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.tenureFlexibility"
>
{{ DailyDepositSchemeProduct.tenureFlexibility | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Lien Marked</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.lienMarked"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.lienMarked"
>
{{ DailyDepositSchemeProduct.lienMarked | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.withdrawalFeeForTransfers"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.withdrawalFeeForTransfers"
>
{{ DailyDepositSchemeProduct.withdrawalFeeForTransfers | yesNo }}
</span>
</span>
</div>
</div>
<h4 class="vsp-sub-h">{{ 'labels.inputs.Lock-in Period' | translate }}</h4>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">Enable Lock-in Period</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.enableLockinPeriod"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.enableLockinPeriod"
>
{{ DailyDepositSchemeProduct.enableLockinPeriod | yesNo }}
</span>
</span>
</div>
@if (DailyDepositSchemeProduct.lockinPeriodFrequency) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Lock-in Period' | translate }}</span>
<span class="vsp-v"
>{{ DailyDepositSchemeProduct.lockinPeriodFrequency }}
{{ DailyDepositSchemeProduct.lockinPeriodFrequencyType?.value }}</span
>
</div>
}
</div>
<h4 class="vsp-sub-h">Maturity Instructions</h4>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">Maturity Instructions</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.maturityInstructions"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.maturityInstructions"
>
{{ DailyDepositSchemeProduct.maturityInstructions | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Credit Maturity Proceeds</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.creditMaturityProceeds"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.creditMaturityProceeds"
>
{{ DailyDepositSchemeProduct.creditMaturityProceeds | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Penalty Grace Period (Missed Installments)</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.penaltyGracePeriodMissedInstallments"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.penaltyGracePeriodMissedInstallments"
>
{{ DailyDepositSchemeProduct.penaltyGracePeriodMissedInstallments | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Panel Request Applicable</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.panelRequestApplicable"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.panelRequestApplicable"
>
{{ DailyDepositSchemeProduct.panelRequestApplicable | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">No Penalty (Nominee / Heir Closure)</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.noPenaltyNomineeHeirClosure"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.noPenaltyNomineeHeirClosure"
>
{{ DailyDepositSchemeProduct.noPenaltyNomineeHeirClosure | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Deceased Settlement</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.deceasedSettlement"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.deceasedSettlement"
>
{{ DailyDepositSchemeProduct.deceasedSettlement | yesNo }}
</span>
</span>
</div>
<div class="vsp-kv">
<span class="vsp-k">Allow Debit Txn After Due Date</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.allowDebitTxnAfterDueDate"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.allowDebitTxnAfterDueDate"
>
{{ DailyDepositSchemeProduct.allowDebitTxnAfterDueDate | yesNo }}
</span>
</span>
</div>
</div>
<h4 class="vsp-sub-h">Additional Rules</h4>
<div class="vsp-grid">
<div class="vsp-kv">
<span class="vsp-k">Account Closed Zero Balance in Day Closing</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.accountClosedZeroBalanceInDayClosing"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.accountClosedZeroBalanceInDayClosing"
>
{{ DailyDepositSchemeProduct.accountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</span>
</div>
@if (DailyDepositSchemeProduct.minBalanceForInterestCalculation) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
@if (DailyDepositSchemeProduct.minDepositTerm) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Minimum Deposit Term' | translate }}</span>
<span class="vsp-v"
>{{ DailyDepositSchemeProduct.minDepositTerm }}
{{ DailyDepositSchemeProduct.minDepositTermType?.value }}</span
>
</div>
}
@if (DailyDepositSchemeProduct.maxDepositTerm) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Maximum Deposit Term' | translate }}</span>
<span class="vsp-v"
>{{ DailyDepositSchemeProduct.maxDepositTerm }}
{{ DailyDepositSchemeProduct.maxDepositTermType?.value }}</span
>
</div>
}
@if (DailyDepositSchemeProduct.inMultipleOfDepositTerm) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}</span>
<span class="vsp-v"
>{{ DailyDepositSchemeProduct.inMultipleOfDepositTerm }} -
{{ DailyDepositSchemeProduct.inMultipleOfDepositTermType?.value }}</span
>
</div>
}
@if (DailyDepositSchemeProduct.isMandatoryDeposit !== undefined) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Daily Deposit Scheme Type' | translate }}</span>
<span class="vsp-v">{{
DailyDepositSchemeProduct.isMandatoryDeposit
? ('labels.text.Mandatory' | translate)
: ('labels.text.Voluntary' | translate)
}}</span>
</div>
}
@if (DailyDepositSchemeProduct.preClosurePenalInterest) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Apply penal interest (less)' | translate }}</span>
<span class="vsp-v"
>{{ DailyDepositSchemeProduct.preClosurePenalInterest }}% On
{{ DailyDepositSchemeProduct.preClosurePenalInterestOnType?.value }}</span
>
</div>
}
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}</span>
<span class="vsp-v">
<span
class="vsp-pill"
[class.vsp-pill--green]="DailyDepositSchemeProduct.withHoldTax"
[class.vsp-pill--gray]="!DailyDepositSchemeProduct.withHoldTax"
>
{{ DailyDepositSchemeProduct.withHoldTax | yesNo }}
</span>
</span>
</div>
@if (DailyDepositSchemeProduct.withHoldTax) {
<div class="vsp-kv">
<span class="vsp-k">{{ 'labels.inputs.Tax Group' | translate }}</span>
<span class="vsp-v">{{ DailyDepositSchemeProduct.taxGroup?.name }}</span>
</div>
}
</div>
</section>
<!-- INTEREST RATE CHART -->
@if (DailyDepositSchemeProduct.activeChart) {
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--amber"><fa-icon icon="chart-line"></fa-icon></div>
<h3>{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
</div>
<!-- Chart meta pills -->
<div class="vsp-chart-meta">
@if (DailyDepositSchemeProduct.activeChart.name) {
<span class="vsp-chart-pill"
><fa-icon icon="tag"></fa-icon> {{ DailyDepositSchemeProduct.activeChart.name }}</span
>
}
<span class="vsp-chart-pill"
><fa-icon icon="calendar"></fa-icon> {{ DailyDepositSchemeProduct.activeChart.fromDate | dateFormat }}</span
>
@if (DailyDepositSchemeProduct.activeChart.endDate) {
<span class="vsp-chart-pill vsp-chart-pill--red"
><fa-icon icon="calendar-times"></fa-icon>
{{ DailyDepositSchemeProduct.activeChart.endDate | dateFormat }}</span
>
}
<span
class="vsp-chart-pill"
[class.vsp-chart-pill--green]="DailyDepositSchemeProduct.activeChart.isPrimaryGroupingByAmount"
>
Primary by Amount: {{ DailyDepositSchemeProduct.activeChart.isPrimaryGroupingByAmount | yesNo }}
</span>
</div>
<div class="vsp-table-wrap">
<table
class="vsp-table"
mat-table
[dataSource]="DailyDepositSchemeProduct.activeChart.chartSlabs"
multiTemplateDataRows
>
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let chartSlab; let i = dataIndex">{{ i + 1 }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.toPeriod ?? '—' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.periodType?.value }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
<td mat-cell *matCellDef="let chartSlab">
<strong>{{ chartSlab.annualInterestRate }}</strong>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
class="vsp-incentive-btn"
type="button"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
<fa-icon [icon]="expandChartSlabIndex === chartSlabIndex ? 'eye-slash' : 'eye'"></fa-icon>
{{
expandChartSlabIndex === chartSlabIndex
? ('labels.buttons.Hide Incentives' | translate)
: ('labels.buttons.View Incentives' | translate)
}}
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
class="vsp-incentive-cell"
>
<div
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
class="vsp-incentive-panel"
>
<p class="vsp-incentive-title">{{ 'labels.heading.Incentives' | translate }}</p>
<div class="vsp-table-wrap">
<table class="vsp-table" mat-table [dataSource]="chartSlab.incentives">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.entityType
| find: DailyDepositSchemeProductTemplate.chartTemplate.entityTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.attributeName
| find
: DailyDepositSchemeProductTemplate.chartTemplate.attributeNameOptions
: 'id'
: 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.conditionType
| find
: DailyDepositSchemeProductTemplate.chartTemplate.conditionTypeOptions
: 'id'
: 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
@switch (incentive.attributeName) {
@case (2) {
<span>{{
incentive.attributeValue
| find: DailyDepositSchemeProductTemplate.chartTemplate.genderOptions : 'id' : 'name'
}}</span>
}
@case (3) {
<span>{{ incentive.attributeValue }}</span>
}
@case (4) {
<span>{{
incentive.attributeValue
| find
: DailyDepositSchemeProductTemplate.chartTemplate.clientTypeOptions
: 'id'
: 'name'
}}</span>
}
@case (5) {
<span>{{
incentive.attributeValue
| find
: DailyDepositSchemeProductTemplate.chartTemplate.clientClassificationOptions
: 'id'
: 'name'
}}</span>
}
}
</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.incentiveType
| find
: DailyDepositSchemeProductTemplate.chartTemplate.incentiveTypeOptions
: 'id'
: 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.amount }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsDisplayedColumns; let i = dataIndex"
[class.vsp-row-even]="i % 2 === 1"
></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
</div>
</section>
}
<!-- CHARGES -->
@if (DailyDepositSchemeProduct.charges.length) {
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--red"><fa-icon icon="receipt"></fa-icon></div>
<h3>{{ 'labels.heading.Charges' | translate }}</h3>
</div>
<div class="vsp-table-wrap">
<table class="vsp-table" mat-table [dataSource]="DailyDepositSchemeProduct.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name }}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeCalculationType.value }}</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="collectedon">
<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>
</section>
}
<!-- ACCOUNTING -->
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--green"><fa-icon icon="book"></fa-icon></div>
<h3>{{ 'labels.heading.Accounting' | translate }}</h3>
</div>
<mifosx-view-savings-accounting-details
[accountingRule]="DailyDepositSchemeProduct.accountingRule"
[accountingMappings]="DailyDepositSchemeProduct.accountingMappings"
></mifosx-view-savings-accounting-details>
</section>
<!-- ADVANCED ACCOUNTING RULES -->
@if (
DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length ||
DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length ||
DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length
) {
<section class="vsp-section">
<div class="vsp-section-h">
<div class="vsp-section-ic vsp-section-ic--purple"><fa-icon icon="sitemap"></fa-icon></div>
<h3>{{ 'labels.inputs.Advanced Accounting Rules' | translate }}</h3>
</div>
@if (DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length) {
<h4 class="vsp-sub-h">{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}</h4>
<div class="vsp-table-wrap">
<table
class="vsp-table"
mat-table
[dataSource]="DailyDepositSchemeProduct.paymentChannelToFundSourceMappings"
>
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let p">{{ p.paymentType.name }}</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let p">{{ p.fundSourceAccount.name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
@if (DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length) {
<h4 class="vsp-sub-h">{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}</h4>
<div class="vsp-table-wrap">
<table class="vsp-table" mat-table [dataSource]="DailyDepositSchemeProduct.feeToIncomeAccountMappings">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fee/Charge' | translate }}</th>
<td mat-cell *matCellDef="let f">{{ f.charge.name }}</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let f">{{ f.incomeAccount.name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
@if (DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length) {
<h4 class="vsp-sub-h">{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}</h4>
<div class="vsp-table-wrap">
<table class="vsp-table" mat-table [dataSource]="DailyDepositSchemeProduct.penaltyToIncomeAccountMappings">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let p">{{ p.charge.name }}</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let p">{{ p.incomeAccount.name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</section>
}
</div>
@@ -0,0 +1,528 @@
/**
* 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/.
*/
:host {
--vsp-navy: #0b1e3d;
--vsp-blue: #1d4ed8;
--vsp-blue-l: #eff6ff;
--vsp-teal: #0e7e6b;
--vsp-teal-l: #ccfbf1;
--vsp-purple: #6d28d9;
--vsp-purple-l: #ede9fe;
--vsp-indigo: #3730a3;
--vsp-indigo-l: #e0e7ff;
--vsp-amber: #b45309;
--vsp-amber-l: #fef3c7;
--vsp-green: #15803d;
--vsp-green-l: #dcfce7;
--vsp-red: #c0392b;
--vsp-red-l: #fee2e2;
--vsp-pink: #be185d;
--vsp-pink-l: #fce7f3;
--vsp-g50: #f8fafc;
--vsp-g100: #f1f5f9;
--vsp-g200: #e2e8f0;
--vsp-g500: #64748b;
--vsp-g700: #334155;
--vsp-g900: #0f172a;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
color: var(--vsp-g900);
font-size: 13.5px;
}
.vsp-root {
padding: 4px 4px 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.vsp-hero {
background: linear-gradient(135deg, var(--vsp-navy) 0%, var(--vsp-blue) 100%);
border-radius: 14px;
padding: 22px 22px 22px 18px;
display: flex;
align-items: flex-start;
gap: 18px;
box-shadow:
0 10px 24px rgb(11 30 61 / 18%),
0 4px 10px rgb(11 30 61 / 12%);
color: #fff;
position: relative;
overflow: hidden;
}
.vsp-hero::before {
content: '';
position: absolute;
top: -60px;
right: -60px;
width: 240px;
height: 240px;
border-radius: 50%;
background: radial-gradient(circle, rgb(255 255 255 / 8%) 0%, transparent 70%);
pointer-events: none;
}
.vsp-hero-left {
display: flex;
align-items: flex-start;
gap: 16px;
flex: 1;
min-width: 0;
}
.vsp-hero-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: rgb(255 255 255 / 16%);
border: 1px solid rgb(255 255 255 / 22%);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.vsp-hero-text {
flex: 1;
min-width: 0;
}
.vsp-hero-eyebrow {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgb(255 255 255 / 70%);
margin-bottom: 4px;
}
.vsp-hero-title {
margin: 0 0 10px;
font-size: 22px;
font-weight: 700;
letter-spacing: 0.01em;
color: #fff;
line-height: 1.2;
}
.vsp-hero-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.vsp-hero-desc {
margin: 12px 0 0;
font-size: 13px;
color: rgb(255 255 255 / 82%);
line-height: 1.5;
max-width: 720px;
}
.vsp-edit-btn {
display: inline-flex;
align-items: center;
gap: 8px;
height: 38px;
padding: 0 16px;
border-radius: 10px;
background: rgb(255 255 255 / 14%);
color: #fff;
border: 1px solid rgb(255 255 255 / 28%);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
font-family: inherit;
flex-shrink: 0;
transition: all 0.16s ease;
fa-icon {
font-size: 12px;
}
&:hover {
background: rgb(255 255 255 / 24%);
border-color: rgb(255 255 255 / 50%);
}
}
.vsp-pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
font-family: inherit;
fa-icon {
font-size: 9.5px;
}
&--code {
background: rgb(255 255 255 / 18%);
color: #fff;
border: 1px solid rgb(255 255 255 / 24%);
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
}
&--teal {
background: var(--vsp-teal-l);
color: var(--vsp-teal);
border: 1px solid #99f6e4;
}
&--blue {
background: var(--vsp-blue-l);
color: var(--vsp-blue);
border: 1px solid #dbeafe;
}
&--green {
background: var(--vsp-green-l);
color: var(--vsp-green);
border: 1px solid #bbf7d0;
}
&--amber {
background: var(--vsp-amber-l);
color: var(--vsp-amber);
border: 1px solid #fde68a;
}
&--red {
background: var(--vsp-red-l);
color: var(--vsp-red);
border: 1px solid #fecaca;
}
&--gray {
background: var(--vsp-g100);
color: var(--vsp-g700);
border: 1px solid var(--vsp-g200);
}
}
.vsp-section {
background: #fff;
border: 1px solid var(--vsp-g200);
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
}
.vsp-section-h {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--vsp-g200);
h3 {
margin: 0;
font-size: 14px;
font-weight: 700;
color: var(--vsp-g900);
letter-spacing: 0.02em;
}
}
.vsp-section-ic {
width: 36px;
height: 36px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
&--blue {
background: var(--vsp-blue-l);
color: var(--vsp-blue);
}
&--teal {
background: var(--vsp-teal-l);
color: var(--vsp-teal);
}
&--purple {
background: var(--vsp-purple-l);
color: var(--vsp-purple);
}
&--indigo {
background: var(--vsp-indigo-l);
color: var(--vsp-indigo);
}
&--amber {
background: var(--vsp-amber-l);
color: var(--vsp-amber);
}
&--green {
background: var(--vsp-green-l);
color: var(--vsp-green);
}
&--red {
background: var(--vsp-red-l);
color: var(--vsp-red);
}
&--pink {
background: var(--vsp-pink-l);
color: var(--vsp-pink);
}
}
.vsp-sub-h {
margin: 16px 0 10px;
font-size: 12px;
font-weight: 700;
color: var(--vsp-g700);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.vsp-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 24px;
}
@media (width <=600px) {
.vsp-grid {
grid-template-columns: 1fr;
}
}
.vsp-kv {
display: flex;
flex-direction: column;
gap: 3px;
padding: 8px 0;
border-bottom: 1px dashed var(--vsp-g100);
&--span {
grid-column: 1 / -1;
}
}
.vsp-k {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--vsp-g500);
}
.vsp-v {
font-size: 13.5px;
color: var(--vsp-g900);
font-weight: 500;
word-break: break-word;
strong {
font-weight: 700;
}
}
.vsp-v--accent {
font-size: 18px;
font-weight: 700;
color: var(--vsp-blue);
letter-spacing: 0.01em;
}
.vsp-suffix {
font-size: 12px;
font-weight: 600;
color: var(--vsp-g500);
margin-left: 4px;
}
.vsp-table-wrap {
overflow-x: auto;
border: 1px solid var(--vsp-g200);
border-radius: 10px;
margin-top: 4px;
}
/* ───────── CHART META PILLS ───────── */
.vsp-chart-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.vsp-chart-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
padding: 5px 12px;
border-radius: 8px;
background: var(--vsp-g100);
border: 1px solid var(--vsp-g200);
color: var(--vsp-g700);
fa-icon {
font-size: 11px;
color: var(--vsp-g500);
}
&--green {
background: var(--vsp-green-l);
border-color: #bbf7d0;
color: var(--vsp-green);
}
&--red {
background: var(--vsp-red-l);
border-color: #fecaca;
color: var(--vsp-red);
}
}
/* ───────── RATE BADGE ───────── */
.vsp-rate-badge {
display: inline-block;
font-size: 13px;
font-weight: 700;
color: var(--vsp-blue);
background: var(--vsp-blue-l);
border: 1px solid #dbeafe;
border-radius: 6px;
padding: 3px 10px;
letter-spacing: 0.02em;
}
/* ───────── CELL SUB TEXT ───────── */
.vsp-cell-sub {
display: block;
font-size: 10.5px;
color: var(--vsp-g500);
margin-top: 2px;
}
/* ───────── INCENTIVE EXPAND ───────── */
.vsp-incentive-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11.5px;
font-weight: 600;
padding: 5px 12px;
border-radius: 8px;
border: 1px solid var(--vsp-g200);
background: var(--vsp-g50);
color: var(--vsp-blue);
cursor: pointer;
font-family: inherit;
transition: all 0.14s ease;
fa-icon {
font-size: 11px;
}
&:hover {
background: var(--vsp-blue-l);
border-color: #dbeafe;
}
}
.vsp-incentive-cell {
padding: 0 !important;
}
.vsp-incentive-panel {
overflow: hidden;
padding: 0 14px;
background: var(--vsp-g50);
border-top: 1px dashed var(--vsp-g200);
}
.vsp-incentive-title {
margin: 12px 0 8px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--vsp-g500);
}
.vsp-table {
width: 100%;
background: #fff;
th.mat-mdc-header-cell {
background: var(--vsp-g50);
color: var(--vsp-g700);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 11px 14px;
border-bottom: 1px solid var(--vsp-g200);
white-space: nowrap;
}
td.mat-mdc-cell {
padding: 10px 14px;
border-bottom: 1px solid var(--vsp-g100);
font-size: 12.5px;
color: var(--vsp-g900);
}
tbody tr:last-child td.mat-mdc-cell {
border-bottom: 0;
}
tbody tr:not(.incentives-row, .vsp-row-even) td.mat-mdc-cell {
background: #fff !important;
}
tbody tr.vsp-row-even td.mat-mdc-cell {
background: var(--vsp-g50) !important;
}
tr.incentives-row {
height: 0;
> td {
padding: 0;
}
.incentives {
overflow: hidden;
}
}
}
@@ -0,0 +1,121 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { trigger, state, transition, animate, style } from '@angular/animations';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
import { FindPipe } from '../../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-daily-deposit-scheme-general-tab',
templateUrl: './daily-deposit-scheme-general-tab.component.html',
styleUrls: ['./daily-deposit-scheme-general-tab.component.scss'],
animations: [
trigger('expandChartSlab', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
],
imports: [
...STANDALONE_SHARED_IMPORTS,
RouterLink,
FaIconComponent,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
ViewSavingsAccountingDetailsComponent,
FindPipe,
DateFormatPipe,
FormatNumberPipe,
YesnoPipe
]
})
export class DailyDepositSchemeGeneralTabComponent {
private route = inject(ActivatedRoute);
DailyDepositSchemeProduct: any;
DailyDepositSchemeProductTemplate: any;
expandChartSlabIndex: number | null = null;
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
readonly chartSlabsBaseColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
get chartSlabsDisplayedColumns(): string[] {
const slabs = this.DailyDepositSchemeProduct?.activeChart?.chartSlabs || [];
const hasIncentives = slabs.some((s: any) => s.incentives?.length);
return hasIncentives ? [
...this.chartSlabsBaseColumns,
'actions'
] : this.chartSlabsBaseColumns;
}
incentivesDisplayedColumns: string[] = [
'entityType',
'attributeName',
'conditionType',
'attributeValue',
'incentiveType',
'amount'
];
chargesDisplayedColumns: string[] = [
'name',
'type',
'amount',
'collectedon'
];
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'incomeAccountId'
];
constructor() {
this.route.data.subscribe((data: { DailyDepositSchemeProduct: any; DailyDepositSchemeProductsTemplate: any }) => {
this.DailyDepositSchemeProduct = data.DailyDepositSchemeProduct;
this.DailyDepositSchemeProductTemplate = data.DailyDepositSchemeProductsTemplate;
});
}
}
@@ -0,0 +1,68 @@
<!--
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="tab-container mat-typography">
@if (!rdInterestRates || rdInterestRates.length === 0) {
<p class="no-rates">No CBS base interest rate versions defined.</p>
}
<mat-accordion multi>
@for (version of rdInterestRates; track version.id) {
<mat-expansion-panel [expanded]="version.isActive">
<mat-expansion-panel-header>
<mat-panel-title>
<span class="rd-version-header">
<strong>{{ version.versionLabel }}</strong>
<span [class]="version.isActive ? 'rd-active-badge' : 'rd-inactive-badge'">
{{ version.isActive ? 'Active' : 'Inactive' }}
</span>
<span style="color: #757575; font-size: 13px">Effective from: {{ version.effectiveFromDate }}</span>
</span>
</mat-panel-title>
</mat-expansion-panel-header>
@if (version.remarks) {
<p>
<em>{{ version.remarks }}</em>
</p>
}
<table class="rd-slab-table mat-elevation-z1" mat-table [dataSource]="version.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="slabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: slabColumns"></tr>
</table>
</mat-expansion-panel>
}
</mat-accordion>
</div>
@@ -0,0 +1,40 @@
/**
* 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/.
*/
.rd-version-header {
display: flex;
align-items: center;
gap: 12px;
}
.rd-active-badge {
background: #4caf50;
color: #fff;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
}
.rd-inactive-badge {
background: #9e9e9e;
color: #fff;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
}
.rd-slab-table {
width: 100%;
margin-top: 8px;
}
.no-rates {
padding: 24px;
color: #757575;
text-align: center;
}
@@ -0,0 +1,66 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DecimalPipe } from '@angular/common';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatExpansionModule } from '@angular/material/expansion';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-rd-interest-rates-tab',
templateUrl: './rd-interest-rates-tab.component.html',
styleUrls: ['./rd-interest-rates-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatExpansionModule,
DecimalPipe
]
})
export class DailyDepositSchemeInterestRatesTabComponent {
private route = inject(ActivatedRoute);
rdInterestRates: any[] = [];
readonly slabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
constructor() {
this.route.data.subscribe((data: { rdInterestRates: any[] }) => {
this.rdInterestRates = data.rdInterestRates || [];
});
}
}
@@ -0,0 +1,16 @@
/**
* 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 { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductsService } from '../../../products.service';
export const rdInterestRatesResolver: ResolveFn<any[]> = (route) => {
const productsService = inject(ProductsService);
return productsService.getRdBaseInterestRates();
};
@@ -0,0 +1,39 @@
<!--
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/.
-->
<mat-card class="product-card">
<mat-card-content>
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
[routerLink]="['./general']"
routerLinkActive
#general="routerLinkActive"
[active]="general.isActive"
>
{{ 'labels.heading.General' | translate }}
</a>
@for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) {
<a
mat-tab-link
[routerLink]="['./datatables', recurringDepositDatatable.registeredTableName]"
routerLinkActive
#datatable="routerLinkActive"
[active]="datatable.isActive"
*mifosxHasPermission="'READ_' + recurringDepositDatatable.registeredTableName"
>
{{ recurringDepositDatatable.registeredTableName }}
</a>
}
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</mat-card-content>
</mat-card>
@@ -0,0 +1,22 @@
/**
* 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/.
*/
.product-card {
margin: 0 auto;
max-width: 80rem;
width: 90%;
padding: 0;
.navigation-tabs {
overflow: auto;
}
i:hover {
cursor: pointer;
}
}
@@ -0,0 +1,44 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet, Router } from '@angular/router';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-view-daily-deposit-scheme-product',
templateUrl: './view-daily-deposit-scheme-product.component.html',
styleUrls: ['./view-daily-deposit-scheme-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTabNav,
MatTabLink,
RouterLinkActive,
RouterLink,
MatTabNavPanel,
RouterOutlet
]
})
export class ViewDailyDepositSchemeProductComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
recurringDepositDatatables: any = [];
constructor() {
this.route.data.subscribe((data: { recurringDepositDatatables: any }) => {
this.recurringDepositDatatables = [];
data.recurringDepositDatatables.forEach((datatable: any) => {
if (datatable.entitySubType === 'Daily Deposit Scheme') {
this.recurringDepositDatatables.push(datatable);
}
});
});
}
}
@@ -30,6 +30,12 @@ import { EditShareProductComponent } from './share-products/edit-share-product/e
import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component'; import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component';
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component'; import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component'; import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component';
import { DailyDepositSchemeProductsComponent } from './daily-deposit-scheme-products/daily-deposit-scheme-products.component';
import { CreateDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component';
import { ViewDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component';
import { DailyDepositSchemeGeneralTabComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component';
import { DailyDepositSchemeDatatableTabComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component';
import { EditDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component';
import { ChargesComponent } from './charges/charges.component'; import { ChargesComponent } from './charges/charges.component';
import { ViewChargeComponent } from './charges/view-charge/view-charge.component'; import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
import { CreateChargeComponent } from './charges/create-charge/create-charge.component'; import { CreateChargeComponent } from './charges/create-charge/create-charge.component';
@@ -75,6 +81,10 @@ import { ShareProductResolver } from './share-products/share-product-resolver';
import { ShareProductAndTemplateResolver } from './share-products/edit-share-product/share-product-and-template.resolver'; import { ShareProductAndTemplateResolver } from './share-products/edit-share-product/share-product-and-template.resolver';
import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver'; import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver';
import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver'; import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver';
import { DailyDepositSchemeProductsResolver } from './daily-deposit-scheme-products/daily-deposit-scheme-products.resolver';
import { DailyDepositSchemeProductsTemplateResolver } from './daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver';
import { DailyDepositSchemeProductResolver } from './daily-deposit-scheme-products/daily-deposit-scheme-product.resolver';
import { DailyDepositSchemeProductAndTemplateResolver } from './daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver';
import { ChargesResolver } from './charges/charges.resolver'; import { ChargesResolver } from './charges/charges.resolver';
import { ChargeResolver } from './charges/charge.resolver'; import { ChargeResolver } from './charges/charge.resolver';
import { ChargesTemplateResolver } from './charges/charges-template.resolver'; import { ChargesTemplateResolver } from './charges/charges-template.resolver';
@@ -724,6 +734,83 @@ const routes: Routes = [
} }
] ]
}, },
{
path: 'daily-deposit-scheme-products',
data: { title: 'Daily Deposit Scheme Products', breadcrumb: 'Daily Deposit Scheme Products' },
children: [
{
path: 'create',
component: CreateDailyDepositSchemeProductComponent,
data: { title: 'Create Daily Deposit Scheme Product', breadcrumb: 'Create' },
resolve: {
dailyDepositSchemeProductsTemplate: DailyDepositSchemeProductsTemplateResolver
}
},
{
path: '',
component: DailyDepositSchemeProductsComponent,
resolve: {
dailyDepositSchemeProducts: DailyDepositSchemeProductsResolver
}
},
{
path: ':productId',
data: {
title: 'View Daily Deposit Scheme Product',
breadcrumb: 'productId',
routeParamBreadcrumb: 'productId'
},
resolve: {
recurringDepositDatatables: SavingProductDatatablesResolver
},
children: [
{
path: '',
component: ViewDailyDepositSchemeProductComponent,
children: [
{
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
component: DailyDepositSchemeGeneralTabComponent,
resolve: {
dailyDepositSchemeProduct: DailyDepositSchemeProductResolver
}
},
{
path: 'datatables',
children: [
{
path: ':datatableName',
component: DailyDepositSchemeDatatableTabComponent,
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' },
resolve: {
recurringDepositDatatable: SavingProductDatatableResolver
}
}
]
}
]
},
{
path: 'edit',
data: {
title: 'Edit Daily Deposit Scheme Product',
breadcrumb: 'edit',
routeParamBreadcrumb: false
},
component: EditDailyDepositSchemeProductComponent,
resolve: {
DailyDepositSchemeProductAndTemplate: DailyDepositSchemeProductAndTemplateResolver
}
}
]
}
]
},
{ {
path: 'fixed-deposit-products', path: 'fixed-deposit-products',
data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' }, data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' },
@@ -1004,6 +1091,10 @@ const routes: Routes = [
ShareProductAndTemplateResolver, ShareProductAndTemplateResolver,
RecurringDepositProductsResolver, RecurringDepositProductsResolver,
RecurringDepositProductResolver, RecurringDepositProductResolver,
DailyDepositSchemeProductsResolver,
DailyDepositSchemeProductsTemplateResolver,
DailyDepositSchemeProductResolver,
DailyDepositSchemeProductAndTemplateResolver,
ChargesResolver, ChargesResolver,
ChargeResolver, ChargeResolver,
ChargesTemplateAndResolver, ChargesTemplateAndResolver,
+19
View File
@@ -125,6 +125,25 @@
</a> </a>
</div> </div>
<div #dailyDepositSchemeProducts>
<a
class="pd-card"
[routerLink]="['daily-deposit-scheme-products']"
*mifosxHasPermission="'READ_RECURRINGDEPOSITPRODUCT'"
>
<div class="pd-card__icon pd-card__icon--teal">
<i class="fas fa-calendar-day"></i>
</div>
<div class="pd-card__body">
<h3 class="pd-card__title">{{ 'labels.heading.Daily Deposit Scheme Products' | translate }}</h3>
<p class="pd-card__desc">
{{ 'labels.text.Add, modify or inactivate a Daily Deposit Scheme product' | translate }}
</p>
</div>
<i class="fas fa-chevron-right pd-card__arrow"></i>
</a>
</div>
<a class="pd-card" [routerLink]="['products-mix']" *mifosxHasPermission="'READ_PRODUCTMIX'"> <a class="pd-card" [routerLink]="['products-mix']" *mifosxHasPermission="'READ_PRODUCTMIX'">
<div class="pd-card__icon pd-card__icon--indigo"> <div class="pd-card__icon pd-card__icon--indigo">
<i class="fas fa-random"></i> <i class="fas fa-random"></i>