DDS: Agent settlement (Agent Tran), Close Party account UI, reports UI, Party-Savings link field
This commit is contained in:
@@ -11,6 +11,10 @@ import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Route } from '../core/route/route.service';
|
||||
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 = [
|
||||
Route.withShell([
|
||||
@@ -41,6 +45,28 @@ const routes: Routes = [
|
||||
title: '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' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1048,14 +1048,16 @@
|
||||
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/recurring-deposits']">
|
||||
<fa-icon icon="sync" class="menu-item-icon"></fa-icon> Recurring
|
||||
</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 class="deposit-menu-group">
|
||||
<div class="deposit-group-title">Agent</div>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Agent Commission
|
||||
</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
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
@@ -1070,6 +1072,9 @@
|
||||
</div>
|
||||
<div class="deposit-menu-group">
|
||||
<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">
|
||||
<fa-icon icon="clock" class="menu-item-icon"></fa-icon> Account Expiry
|
||||
</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 & 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 & 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 & 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 & 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 & 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 & 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 { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.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 { CreateEmployeeComponent } from './employees/create-employee/create-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',
|
||||
data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' },
|
||||
|
||||
@@ -90,6 +90,47 @@
|
||||
</a>
|
||||
</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 & 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 -->
|
||||
<div #manageLoanOriginators>
|
||||
<a class="org-card" [routerLink]="['manage-loan-originators']" *mifosxHasPermission="'READ_LOAN_ORIGINATOR'">
|
||||
|
||||
@@ -233,6 +233,141 @@ export class OrganizationService {
|
||||
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
|
||||
* @returns {Observable<any>} Currencies data
|
||||
|
||||
+131
@@ -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>
|
||||
+138
@@ -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;
|
||||
}
|
||||
}
|
||||
+346
@@ -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;
|
||||
}
|
||||
}
|
||||
+35
@@ -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();
|
||||
}
|
||||
}
|
||||
+121
@@ -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>
|
||||
+303
@@ -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%;
|
||||
}
|
||||
}
|
||||
+423
@@ -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);
|
||||
}
|
||||
}
|
||||
+85
@@ -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>
|
||||
+69
@@ -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%;
|
||||
}
|
||||
}
|
||||
+103
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+89
@@ -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 & 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>
|
||||
+102
@@ -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;
|
||||
}
|
||||
}
|
||||
+104
@@ -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;
|
||||
}
|
||||
}
|
||||
+77
@@ -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>
|
||||
+81
@@ -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;
|
||||
}
|
||||
+177
@@ -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;
|
||||
}
|
||||
}
|
||||
+109
@@ -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 <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> | {{ _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>
|
||||
+215
@@ -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;
|
||||
}
|
||||
+717
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+732
@@ -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>
|
||||
+131
@@ -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;
|
||||
}
|
||||
}
|
||||
+142
@@ -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;
|
||||
}
|
||||
}
|
||||
+123
@@ -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>
|
||||
+164
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -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;
|
||||
}
|
||||
}
|
||||
+192
@@ -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 & 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>
|
||||
+151
@@ -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;
|
||||
}
|
||||
+169
@@ -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;
|
||||
}
|
||||
}
|
||||
+37
@@ -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);
|
||||
}
|
||||
}
|
||||
+172
@@ -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>
|
||||
+17
@@ -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;
|
||||
}
|
||||
+124
@@ -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));
|
||||
}
|
||||
}
|
||||
+35
@@ -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();
|
||||
}
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
+202
@@ -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>
|
||||
+138
@@ -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;
|
||||
}
|
||||
}
|
||||
+305
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+7
@@ -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/.
|
||||
*/
|
||||
+38
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+843
@@ -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>
|
||||
+528
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+68
@@ -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>
|
||||
+40
@@ -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;
|
||||
}
|
||||
+66
@@ -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 || [];
|
||||
});
|
||||
}
|
||||
}
|
||||
+16
@@ -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();
|
||||
};
|
||||
+39
@@ -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>
|
||||
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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 { 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 { 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 { ViewChargeComponent } from './charges/view-charge/view-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 { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.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 { ChargeResolver } from './charges/charge.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',
|
||||
data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' },
|
||||
@@ -1004,6 +1091,10 @@ const routes: Routes = [
|
||||
ShareProductAndTemplateResolver,
|
||||
RecurringDepositProductsResolver,
|
||||
RecurringDepositProductResolver,
|
||||
DailyDepositSchemeProductsResolver,
|
||||
DailyDepositSchemeProductsTemplateResolver,
|
||||
DailyDepositSchemeProductResolver,
|
||||
DailyDepositSchemeProductAndTemplateResolver,
|
||||
ChargesResolver,
|
||||
ChargeResolver,
|
||||
ChargesTemplateAndResolver,
|
||||
|
||||
@@ -125,6 +125,25 @@
|
||||
</a>
|
||||
</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'">
|
||||
<div class="pd-card__icon pd-card__icon--indigo">
|
||||
<i class="fas fa-random"></i>
|
||||
|
||||
Reference in New Issue
Block a user