Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a61b3057 |
@@ -11,6 +11,10 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { Route } from '../core/route/route.service';
|
import { Route } from '../core/route/route.service';
|
||||||
import { AccountsListComponent } from './accounts-list/accounts-list.component';
|
import { AccountsListComponent } from './accounts-list/accounts-list.component';
|
||||||
|
import { PartyMasterComponent } from '../deposits/party-master/party-master.component';
|
||||||
|
import { DailyTransactionEntryComponent } from '../deposits/daily-transaction-entry/daily-transaction-entry.component';
|
||||||
|
import { DdsReportsComponent } from '../deposits/dds-reports/dds-reports.component';
|
||||||
|
import { ReportViewerComponent } from '../deposits/dds-reports/report-viewer/report-viewer.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -41,6 +45,28 @@ const routes: Routes = [
|
|||||||
title: 'Recurring Deposit Accounts',
|
title: 'Recurring Deposit Accounts',
|
||||||
breadcrumb: 'Recurring Deposit Accounts'
|
breadcrumb: 'Recurring Deposit Accounts'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dds',
|
||||||
|
component: PartyMasterComponent,
|
||||||
|
data: { title: 'Daily Deposit Scheme — Party Master', breadcrumb: 'Party Master' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dds-collection',
|
||||||
|
component: DailyTransactionEntryComponent,
|
||||||
|
data: { title: 'Daily Transaction Entry', breadcrumb: 'Daily Transaction Entry' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dds-reports',
|
||||||
|
data: { title: 'DDS Reports', breadcrumb: 'DDS Reports' },
|
||||||
|
children: [
|
||||||
|
{ path: '', component: DdsReportsComponent },
|
||||||
|
{
|
||||||
|
path: 'view/:reportName',
|
||||||
|
component: ReportViewerComponent,
|
||||||
|
data: { title: 'DDS Report', breadcrumb: 'reportName', routeParamBreadcrumb: 'reportName' }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1048,14 +1048,16 @@
|
|||||||
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/recurring-deposits']">
|
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/recurring-deposits']">
|
||||||
<fa-icon icon="sync" class="menu-item-icon"></fa-icon> Recurring
|
<fa-icon icon="sync" class="menu-item-icon"></fa-icon> Recurring
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0"><fa-icon icon="coins" class="menu-item-icon"></fa-icon> DDS</button>
|
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/dds']">
|
||||||
|
<fa-icon icon="coins" class="menu-item-icon"></fa-icon> DDS
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Agent</div>
|
<div class="deposit-group-title">Agent</div>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0">
|
||||||
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Agent Commission
|
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Agent Commission
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/dds-collection']">
|
||||||
<fa-icon icon="calendar-check" class="menu-item-icon"></fa-icon> Today Collection
|
<fa-icon icon="calendar-check" class="menu-item-icon"></fa-icon> Today Collection
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0">
|
||||||
@@ -1070,6 +1072,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Reports</div>
|
<div class="deposit-group-title">Reports</div>
|
||||||
|
<button mat-menu-item tabindex="0" [routerLink]="['/accounts/dds-reports']">
|
||||||
|
<fa-icon icon="coins" class="menu-item-icon"></fa-icon> DDS Reports
|
||||||
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0">
|
||||||
<fa-icon icon="clock" class="menu-item-icon"></fa-icon> Account Expiry
|
<fa-icon icon="clock" class="menu-item-icon"></fa-icon> Account Expiry
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="dte-container">
|
||||||
|
<div class="dte-header">
|
||||||
|
<button mat-stroked-button [routerLink]="['../']" class="dte-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> Deposits
|
||||||
|
</button>
|
||||||
|
<h2 class="dte-header__title">Daily Transaction Detail</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dte-card">
|
||||||
|
<form [formGroup]="entryForm">
|
||||||
|
<div class="dte-info-panels">
|
||||||
|
<div class="dte-info-panel">
|
||||||
|
<div class="dte-info-panel__title">Agent Information</div>
|
||||||
|
<dl>
|
||||||
|
<dt>Name</dt>
|
||||||
|
<dd>{{ agentInfo.name || '—' }}</dd>
|
||||||
|
<dt>Address</dt>
|
||||||
|
<dd>{{ agentInfo.address || '—' }}</dd>
|
||||||
|
<dt>City</dt>
|
||||||
|
<dd>{{ agentInfo.city || '—' }}</dd>
|
||||||
|
<dt>A/C</dt>
|
||||||
|
<dd>{{ agentInfo.accountNo || '—' }}</dd>
|
||||||
|
<dt>Guarantor</dt>
|
||||||
|
<dd>{{ agentInfo.guarantor1 || '—' }}</dd>
|
||||||
|
<dt>Guarantor</dt>
|
||||||
|
<dd>{{ agentInfo.guarantor2 || '—' }}</dd>
|
||||||
|
<dt>Curr Bal</dt>
|
||||||
|
<dd>{{ agentInfo.currentBalance | number: '1.2-2' }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dte-info-panel">
|
||||||
|
<div class="dte-info-panel__title">Party Information</div>
|
||||||
|
<p class="dte-info-panel__hint">Populated once a party is selected in a collection row below.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dte-controls">
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Agent</mat-label>
|
||||||
|
<input matInput required formControlName="agentId" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-stroked-button type="button" (click)="loadAgent()"><i class="fas fa-search"></i> Load</button>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Date</mat-label>
|
||||||
|
<input matInput required [matDatepicker]="txnDatePicker" formControlName="transactionDate" />
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="txnDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #txnDatePicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Tran. Type</mat-label>
|
||||||
|
<mat-select required formControlName="transactionType">
|
||||||
|
@for (type of transactionTypes; track type) {
|
||||||
|
<mat-option [value]="type">{{ type }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isPartyTran) {
|
||||||
|
<div class="dte-section">
|
||||||
|
<div class="dte-section__header">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
<h3>Today's Collections</h3>
|
||||||
|
<button mat-stroked-button type="button" class="dte-add-row" (click)="addCollectionRow()">
|
||||||
|
<i class="fas fa-plus"></i> Add Row
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (collections.length === 0) {
|
||||||
|
<p class="dte-empty">No collection rows added yet.</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (row of collections.controls; track row; let i = $index) {
|
||||||
|
<div class="dte-collection-row" [formGroup]="$any(row)">
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Party Code</mat-label>
|
||||||
|
<input matInput required formControlName="partyCode" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>TRTP</mat-label>
|
||||||
|
<mat-select required formControlName="transactionMode">
|
||||||
|
@for (mode of transactionModes; track mode) {
|
||||||
|
<mat-option [value]="mode">{{ mode }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>AC Type</mat-label>
|
||||||
|
<input matInput formControlName="accountType" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>A/C No.</mat-label>
|
||||||
|
<input matInput required formControlName="accountNo" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Amount</mat-label>
|
||||||
|
<input matInput required type="number" formControlName="amount" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Cheque No</mat-label>
|
||||||
|
<input matInput formControlName="chequeNo" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Token No</mat-label>
|
||||||
|
<input matInput formControlName="tokenNo" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field dte-field--wide">
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<input matInput formControlName="description" />
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
class="dte-remove-row"
|
||||||
|
(click)="removeCollectionRow(i)"
|
||||||
|
matTooltip="Remove row"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isAgentTran) {
|
||||||
|
<div class="dte-section">
|
||||||
|
<div class="dte-section__header">
|
||||||
|
<i class="fas fa-hand-holding-usd"></i>
|
||||||
|
<h3>Agent Settlement</h3>
|
||||||
|
</div>
|
||||||
|
<p class="dte-info-panel__hint">
|
||||||
|
Cash the agent hands over to the branch, deducted from the agent's collected balance shown above.
|
||||||
|
</p>
|
||||||
|
<div class="dte-grid">
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Amount</mat-label>
|
||||||
|
<input matInput required type="number" formControlName="settlementAmount" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>TRTP</mat-label>
|
||||||
|
<mat-select required formControlName="settlementMode">
|
||||||
|
@for (mode of transactionModes; track mode) {
|
||||||
|
<mat-option [value]="mode">{{ mode }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>AC Type</mat-label>
|
||||||
|
<input matInput formControlName="settlementAccountType" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>A/C No.</mat-label>
|
||||||
|
<input matInput formControlName="settlementAccountNo" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field dte-field--wide">
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<input matInput formControlName="settlementDescription" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isWithdrawDeposite) {
|
||||||
|
<div class="dte-section">
|
||||||
|
<div class="dte-section__header">
|
||||||
|
<i class="fas fa-exchange-alt"></i>
|
||||||
|
<h3>Withdraw Deposite</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dte-grid">
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Creditor Party Code</mat-label>
|
||||||
|
<input matInput formControlName="creditorPartyCode" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Creditor Party Name</mat-label>
|
||||||
|
<input matInput formControlName="creditorPartyName" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Creditor Curr Bal</mat-label>
|
||||||
|
<input matInput formControlName="creditorPartyBalance" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Debit Party Code</mat-label>
|
||||||
|
<input matInput formControlName="debitPartyCode" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Debit Party Name</mat-label>
|
||||||
|
<input matInput formControlName="debitPartyName" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Debit Curr Bal</mat-label>
|
||||||
|
<input matInput formControlName="debitPartyBalance" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="dte-field">
|
||||||
|
<mat-label>Amount</mat-label>
|
||||||
|
<input matInput type="number" formControlName="withdrawAmount" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="dte-field dte-field--wide">
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<input matInput formControlName="withdrawDescription" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!isPartyTran && !isWithdrawDeposite && !isAgentTran) {
|
||||||
|
<div class="dte-section">
|
||||||
|
<p class="dte-empty">
|
||||||
|
"{{ entryForm.get('transactionType').value }}" entry screen — same shape as Party Tran, to be filled in once
|
||||||
|
the backend distinguishes these transaction types.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="dte-action-bar">
|
||||||
|
<button mat-raised-button type="button" (click)="exit()">Exit</button>
|
||||||
|
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
|
||||||
|
<button mat-stroked-button type="button" (click)="retrieve()">Retrieve</button>
|
||||||
|
<button mat-stroked-button type="button" (click)="viewLedger()">View Ledger</button>
|
||||||
|
<button mat-stroked-button type="button" (click)="signature()">Signature</button>
|
||||||
|
<button mat-raised-button color="primary" type="button" [disabled]="entryForm.invalid" (click)="save()">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$primary: #0f766e;
|
||||||
|
|
||||||
|
.dte-container {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-back {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-card {
|
||||||
|
background: var(--md-sys-color-surface, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-info-panels {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-info-panel {
|
||||||
|
border: 1px solid rgb(0 0 0 / 10%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgb(15 118 110 / 4%);
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: $primary;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(0 0 0 / 50%);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 4px 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(0 0 0 / 60%);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-field {
|
||||||
|
min-width: 180px;
|
||||||
|
|
||||||
|
&--wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-section {
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-add-row {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-empty {
|
||||||
|
color: rgb(0 0 0 / 50%);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-collection-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr) auto;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-top: 1px dashed rgb(0 0 0 / 10%);
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-collection-row .dte-field--wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-remove-row {
|
||||||
|
color: #b91c1c;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1000px) {
|
||||||
|
.dte-info-panels {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dte-collection-row {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Angular Imports */
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
import { Dates } from 'app/core/utils/dates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily Transaction Entry component.
|
||||||
|
*
|
||||||
|
* The screen a collection agent uses at the end of a day's round: pick a
|
||||||
|
* transaction type, then batch-enter that day's collections against
|
||||||
|
* assigned parties (Party Tran), settle collected cash to the branch
|
||||||
|
* (Agent Tran), or record a withdrawal/transfer between accounts (Withdraw
|
||||||
|
* Deposite). Modelled directly on the legacy Daily Transaction Detail
|
||||||
|
* screen this replaces.
|
||||||
|
*
|
||||||
|
* Backed by the DDS Party Transaction API (`/dds/partytransactions`) for
|
||||||
|
* Party Tran, Agent Tran and Withdraw Deposite - the three types this
|
||||||
|
* screen actually collects fields for; Party Payment / OD Entry / OD
|
||||||
|
* Payment remain placeholders until the backend distinguishes them too.
|
||||||
|
* "Retrieve" (a ready-made list of today's assigned parties) isn't wired -
|
||||||
|
* that needs a dedicated per-agent Party lookup this MVP doesn't have yet.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-daily-transaction-entry',
|
||||||
|
templateUrl: './daily-transaction-entry.component.html',
|
||||||
|
styleUrls: ['./daily-transaction-entry.component.scss'],
|
||||||
|
imports: [...STANDALONE_SHARED_IMPORTS]
|
||||||
|
})
|
||||||
|
export class DailyTransactionEntryComponent {
|
||||||
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
|
private router = inject(Router);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
|
private settingsService = inject(SettingsService);
|
||||||
|
private dateUtils = inject(Dates);
|
||||||
|
|
||||||
|
private static readonly TRANSACTION_TYPE_CODES: Record<string, string> = {
|
||||||
|
'Party Tran': 'PARTY_TRAN',
|
||||||
|
'Agent Tran': 'AGENT_TRAN',
|
||||||
|
'Withdraw Deposite': 'WITHDRAW_DEPOSITE'
|
||||||
|
};
|
||||||
|
|
||||||
|
transactionTypes = [
|
||||||
|
'Party Tran',
|
||||||
|
'Agent Tran',
|
||||||
|
'Party Payment',
|
||||||
|
'OD Entry',
|
||||||
|
'OD Payment',
|
||||||
|
'Withdraw Deposite'
|
||||||
|
];
|
||||||
|
|
||||||
|
transactionModes = [
|
||||||
|
'Cash',
|
||||||
|
'Transfer',
|
||||||
|
'Clearing'
|
||||||
|
];
|
||||||
|
|
||||||
|
entryForm: UntypedFormGroup = this.formBuilder.group({
|
||||||
|
agentId: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
transactionType: [
|
||||||
|
'Party Tran',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
transactionDate: [
|
||||||
|
new Date(),
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
// Withdraw Deposite fields
|
||||||
|
creditorPartyCode: [''],
|
||||||
|
creditorPartyName: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
creditorPartyBalance: [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
debitPartyCode: [''],
|
||||||
|
debitPartyName: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
debitPartyBalance: [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
withdrawAmount: [0],
|
||||||
|
withdrawDescription: [''],
|
||||||
|
// Agent Tran fields
|
||||||
|
settlementAmount: [0],
|
||||||
|
settlementMode: ['Cash'],
|
||||||
|
settlementAccountType: [''],
|
||||||
|
settlementAccountNo: [''],
|
||||||
|
settlementDescription: [''],
|
||||||
|
collections: this.formBuilder.array([])
|
||||||
|
});
|
||||||
|
|
||||||
|
agentInfo = {
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
city: '',
|
||||||
|
accountNo: '',
|
||||||
|
guarantor1: '',
|
||||||
|
guarantor2: '',
|
||||||
|
currentBalance: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
get isPartyTran(): boolean {
|
||||||
|
return this.entryForm.get('transactionType').value === 'Party Tran';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isWithdrawDeposite(): boolean {
|
||||||
|
return this.entryForm.get('transactionType').value === 'Withdraw Deposite';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAgentTran(): boolean {
|
||||||
|
return this.entryForm.get('transactionType').value === 'Agent Tran';
|
||||||
|
}
|
||||||
|
|
||||||
|
get collections(): UntypedFormArray {
|
||||||
|
return this.entryForm.get('collections') as UntypedFormArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCollectionRow(): UntypedFormGroup {
|
||||||
|
return this.formBuilder.group({
|
||||||
|
partyCode: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
transactionMode: [
|
||||||
|
'Cash',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
accountType: [''],
|
||||||
|
accountNo: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
0,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
chequeNo: [''],
|
||||||
|
tokenNo: [''],
|
||||||
|
description: [''],
|
||||||
|
partyName: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
balanceAfterTransaction: [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCollectionRow(): void {
|
||||||
|
this.collections.push(this.buildCollectionRow());
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCollectionRow(index: number): void {
|
||||||
|
this.collections.removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAgent(): void {
|
||||||
|
const agentId = this.entryForm.get('agentId')?.value;
|
||||||
|
if (!agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.organizationService.getAgent(agentId).subscribe((agent: any) => {
|
||||||
|
const guarantors = agent.guarantors || [];
|
||||||
|
this.agentInfo = {
|
||||||
|
name: agent.agentNameId || '',
|
||||||
|
address: agent.agentAddress1 || '',
|
||||||
|
city: agent.agentCity || '',
|
||||||
|
accountNo: agent.agentSavingAccountNo || '',
|
||||||
|
guarantor1: guarantors[0]?.guarantorName || '',
|
||||||
|
guarantor2: guarantors[1]?.guarantorName || '',
|
||||||
|
currentBalance: agent.agentCurrentBalance || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieve(): void {
|
||||||
|
// No "my assigned parties" read API exists yet — this will pre-fill
|
||||||
|
// the collections grid with today's assigned parties once available.
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLedger(): void {
|
||||||
|
// No ledger view exists yet for this agent.
|
||||||
|
}
|
||||||
|
|
||||||
|
signature(): void {
|
||||||
|
// No signature capture/verification flow exists yet.
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.entryForm.reset({
|
||||||
|
transactionType: 'Party Tran',
|
||||||
|
transactionDate: new Date()
|
||||||
|
});
|
||||||
|
this.collections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(): void {
|
||||||
|
this.router.navigate(['/accounts/savings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
const raw = this.entryForm.getRawValue();
|
||||||
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
|
const locale = this.settingsService.language.code;
|
||||||
|
const transactionType = DailyTransactionEntryComponent.TRANSACTION_TYPE_CODES[raw.transactionType];
|
||||||
|
|
||||||
|
const payload: any = {
|
||||||
|
agentId: Number(raw.agentId),
|
||||||
|
transactionType,
|
||||||
|
transactionDate: this.dateUtils.formatDate(raw.transactionDate, dateFormat),
|
||||||
|
dateFormat,
|
||||||
|
locale
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isPartyTran) {
|
||||||
|
payload.collections = (raw.collections || []).map((row: any) => ({
|
||||||
|
partyCode: row.partyCode,
|
||||||
|
transactionMode: row.transactionMode,
|
||||||
|
accountType: row.accountType || undefined,
|
||||||
|
accountNo: row.accountNo,
|
||||||
|
amount: row.amount,
|
||||||
|
chequeNo: row.chequeNo || undefined,
|
||||||
|
tokenNo: row.tokenNo || undefined,
|
||||||
|
description: row.description || undefined
|
||||||
|
}));
|
||||||
|
} else if (this.isWithdrawDeposite) {
|
||||||
|
payload.creditorPartyCode = raw.creditorPartyCode;
|
||||||
|
payload.debitPartyCode = raw.debitPartyCode;
|
||||||
|
payload.withdrawAmount = raw.withdrawAmount;
|
||||||
|
payload.withdrawDescription = raw.withdrawDescription || undefined;
|
||||||
|
} else if (this.isAgentTran) {
|
||||||
|
payload.amount = raw.settlementAmount;
|
||||||
|
payload.transactionMode = raw.settlementMode;
|
||||||
|
payload.accountType = raw.settlementAccountType || undefined;
|
||||||
|
payload.accountNo = raw.settlementAccountNo || undefined;
|
||||||
|
payload.description = raw.settlementDescription || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.organizationService.createPartyTransaction(payload).subscribe(() => {
|
||||||
|
this.collections.clear();
|
||||||
|
this.entryForm.patchValue({
|
||||||
|
creditorPartyCode: '',
|
||||||
|
debitPartyCode: '',
|
||||||
|
withdrawAmount: 0,
|
||||||
|
withdrawDescription: '',
|
||||||
|
settlementAmount: 0,
|
||||||
|
settlementAccountType: '',
|
||||||
|
settlementAccountNo: '',
|
||||||
|
settlementDescription: ''
|
||||||
|
});
|
||||||
|
this.loadAgent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="dr-container">
|
||||||
|
<div class="dr-header">
|
||||||
|
<button mat-stroked-button [routerLink]="['../']" class="dr-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> Deposits
|
||||||
|
</button>
|
||||||
|
<h2 class="dr-header__title">DDS Reports</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (group of reportGroups; track group.title) {
|
||||||
|
<div class="dr-group">
|
||||||
|
<div class="dr-group__title">{{ group.title }}</div>
|
||||||
|
<div class="dr-grid">
|
||||||
|
@for (report of group.reports; track report) {
|
||||||
|
<a class="dr-card" [routerLink]="['view', report]">
|
||||||
|
<div class="dr-card__icon">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</div>
|
||||||
|
<div class="dr-card__title">{{ report }}</div>
|
||||||
|
<i class="fas fa-chevron-right dr-card__arrow"></i>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$primary: #0f766e;
|
||||||
|
|
||||||
|
.dr-container {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dr-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dr-back {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dr-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dr-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dr-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: var(--md-sys-color-surface, #fff);
|
||||||
|
border: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: $primary;
|
||||||
|
font-size: 16px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
color: rgb(0 0 0 / 30%);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
.dr-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.dr-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Angular Imports */
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DDS Reports hub component.
|
||||||
|
*
|
||||||
|
* Lists every DDS report from the legacy Reports menu, grouped the same
|
||||||
|
* way (As-On-Date, Interest, Daily/Balance/Master, Transaction). Each
|
||||||
|
* links to the shared report viewer, parameterised by report name.
|
||||||
|
*
|
||||||
|
* UI only for now: no report actually queries the backend yet — see
|
||||||
|
* ReportViewerComponent.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-dds-reports',
|
||||||
|
templateUrl: './dds-reports.component.html',
|
||||||
|
styleUrls: ['./dds-reports.component.scss'],
|
||||||
|
imports: [...STANDALONE_SHARED_IMPORTS]
|
||||||
|
})
|
||||||
|
export class DdsReportsComponent {
|
||||||
|
reportGroups = [
|
||||||
|
{
|
||||||
|
title: 'As On Date',
|
||||||
|
reports: [
|
||||||
|
'Supplementary-DDS (As On)',
|
||||||
|
'Agent Check List',
|
||||||
|
'Party Check List-As On',
|
||||||
|
'Party Balance',
|
||||||
|
'Agent Wise Balance',
|
||||||
|
'Agent Wise Monthly Collection',
|
||||||
|
'Commission Register',
|
||||||
|
'Collection Summary Agent Wise',
|
||||||
|
'Close Party Ac Register',
|
||||||
|
'Agent Partywise Daily Collection',
|
||||||
|
'Daily Collection Balance',
|
||||||
|
'Interest Certificate',
|
||||||
|
'Mature Date Wise Report'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Interest',
|
||||||
|
reports: [
|
||||||
|
'Party Int. Register',
|
||||||
|
'Commission Register'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Daily / Balance / Master',
|
||||||
|
reports: [
|
||||||
|
'Daily Collection Report',
|
||||||
|
'Balance Summary',
|
||||||
|
'Master Data Export'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transaction',
|
||||||
|
reports: ['Transaction Register']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Statements',
|
||||||
|
reports: ['Agent Commission Statement']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="rv-container">
|
||||||
|
<div class="rv-header">
|
||||||
|
<button mat-stroked-button [routerLink]="['../../']" class="rv-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> DDS Reports
|
||||||
|
</button>
|
||||||
|
<h2 class="rv-header__title">{{ reportName }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rv-card">
|
||||||
|
<form [formGroup]="filterForm">
|
||||||
|
<div class="rv-grid">
|
||||||
|
<mat-form-field appearance="outline" class="rv-field">
|
||||||
|
<mat-label>From Date</mat-label>
|
||||||
|
<input matInput required [matDatepicker]="fromPicker" formControlName="fromDate" />
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #fromPicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="rv-field">
|
||||||
|
<mat-label>To Date</mat-label>
|
||||||
|
<input matInput required [matDatepicker]="toPicker" formControlName="toDate" />
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #toPicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="rv-field">
|
||||||
|
<mat-label>Agent From</mat-label>
|
||||||
|
<input matInput type="number" formControlName="agentFrom" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="rv-field">
|
||||||
|
<mat-label>Agent To</mat-label>
|
||||||
|
<input matInput type="number" formControlName="agentTo" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="rv-field">
|
||||||
|
<mat-label>Party From</mat-label>
|
||||||
|
<input matInput type="number" formControlName="partyFrom" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="rv-field">
|
||||||
|
<mat-label>Party To</mat-label>
|
||||||
|
<input matInput type="number" formControlName="partyTo" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rv-action-bar">
|
||||||
|
<button mat-raised-button type="button" (click)="exit()">Exit</button>
|
||||||
|
<button mat-raised-button color="primary" type="button" [disabled]="filterForm.invalid" (click)="runReport()">
|
||||||
|
<i class="fas fa-play" style="margin-right: 6px"></i> Run Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (hasRun) {
|
||||||
|
<div class="rv-results">
|
||||||
|
@if (!available) {
|
||||||
|
<p class="rv-empty">{{ unavailableMessage }}</p>
|
||||||
|
} @else if (rows.length === 0) {
|
||||||
|
<p class="rv-empty">No rows matched these filters.</p>
|
||||||
|
} @else {
|
||||||
|
<div class="rv-table-scroll">
|
||||||
|
<table class="rv-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
@for (column of columns; track column) {
|
||||||
|
<th>{{ column }}</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (row of rows; track row) {
|
||||||
|
<tr>
|
||||||
|
@for (column of columns; track column) {
|
||||||
|
<td>{{ row[column] }}</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$primary: #0f766e;
|
||||||
|
|
||||||
|
.rv-container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-back {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-card {
|
||||||
|
background: var(--md-sys-color-surface, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-results {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-empty {
|
||||||
|
color: rgb(0 0 0 / 50%);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-table-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rv-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: rgb(0 0 0 / 55%);
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 2px solid rgb(0 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid rgb(0 0 0 / 6%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
.rv-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Angular Imports */
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
|
import { Dates } from 'app/core/utils/dates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic DDS report viewer.
|
||||||
|
*
|
||||||
|
* One shared screen for all 13+ DDS reports — the report name comes from
|
||||||
|
* the route and is mapped to a backend report key. Every report in the
|
||||||
|
* legacy system shared the same basic filter shape (date, agent range,
|
||||||
|
* party range), so this avoids building a bespoke screen per report - the
|
||||||
|
* backend returns a generic {columns, rows} shape this renders as one
|
||||||
|
* table regardless of which report it is.
|
||||||
|
*
|
||||||
|
* One legacy report (Close Party Ac Register) has no corresponding data in
|
||||||
|
* the backend yet - Party accounts have no closed/open status at all. Run
|
||||||
|
* Report still calls through, and the backend's `available: false` +
|
||||||
|
* message is shown instead of a table.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-dds-report-viewer',
|
||||||
|
templateUrl: './report-viewer.component.html',
|
||||||
|
styleUrls: ['./report-viewer.component.scss'],
|
||||||
|
imports: [...STANDALONE_SHARED_IMPORTS]
|
||||||
|
})
|
||||||
|
export class ReportViewerComponent {
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
private router = inject(Router);
|
||||||
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
|
private organizationService = inject(OrganizationService);
|
||||||
|
private dateUtils = inject(Dates);
|
||||||
|
|
||||||
|
private static readonly REPORT_KEYS: Record<string, string> = {
|
||||||
|
'Supplementary-DDS (As On)': 'SUPPLEMENTARY_DDS',
|
||||||
|
'Agent Check List': 'AGENT_CHECK_LIST',
|
||||||
|
'Party Check List-As On': 'PARTY_CHECK_LIST',
|
||||||
|
'Party Balance': 'PARTY_BALANCE',
|
||||||
|
'Agent Wise Balance': 'AGENT_WISE_BALANCE',
|
||||||
|
'Agent Wise Monthly Collection': 'AGENT_WISE_MONTHLY_COLLECTION',
|
||||||
|
'Commission Register': 'COMMISSION_REGISTER',
|
||||||
|
'Collection Summary Agent Wise': 'COLLECTION_SUMMARY_AGENT_WISE',
|
||||||
|
'Close Party Ac Register': 'CLOSE_PARTY_AC_REGISTER',
|
||||||
|
'Agent Partywise Daily Collection': 'AGENT_PARTYWISE_DAILY_COLLECTION',
|
||||||
|
'Daily Collection Balance': 'DAILY_COLLECTION_REPORT',
|
||||||
|
'Interest Certificate': 'INTEREST_CERTIFICATE',
|
||||||
|
'Mature Date Wise Report': 'MATURE_DATE_WISE',
|
||||||
|
'Party Int. Register': 'PARTY_INTEREST_REGISTER',
|
||||||
|
'Daily Collection Report': 'DAILY_COLLECTION_REPORT',
|
||||||
|
'Balance Summary': 'BALANCE_SUMMARY',
|
||||||
|
'Master Data Export': 'MASTER_DATA_EXPORT',
|
||||||
|
'Transaction Register': 'TRANSACTION_REGISTER',
|
||||||
|
'Agent Commission Statement': 'AGENT_COMMISSION_STATEMENT'
|
||||||
|
};
|
||||||
|
|
||||||
|
reportName: string = this.route.snapshot.paramMap.get('reportName') || 'Report';
|
||||||
|
|
||||||
|
hasRun = false;
|
||||||
|
available = true;
|
||||||
|
unavailableMessage = '';
|
||||||
|
columns: string[] = [];
|
||||||
|
rows: Array<Record<string, any>> = [];
|
||||||
|
|
||||||
|
filterForm: UntypedFormGroup = this.formBuilder.group({
|
||||||
|
fromDate: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
toDate: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
agentFrom: [1],
|
||||||
|
agentTo: [99999],
|
||||||
|
partyFrom: [1],
|
||||||
|
partyTo: [99999999]
|
||||||
|
});
|
||||||
|
|
||||||
|
runReport(): void {
|
||||||
|
const reportKey = ReportViewerComponent.REPORT_KEYS[this.reportName];
|
||||||
|
if (!reportKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = this.filterForm.getRawValue();
|
||||||
|
|
||||||
|
this.organizationService
|
||||||
|
.runDdsReport(reportKey, {
|
||||||
|
fromDate: this.dateUtils.getDate(raw.fromDate),
|
||||||
|
toDate: this.dateUtils.getDate(raw.toDate),
|
||||||
|
agentFrom: raw.agentFrom,
|
||||||
|
agentTo: raw.agentTo,
|
||||||
|
partyFrom: raw.partyFrom,
|
||||||
|
partyTo: raw.partyTo
|
||||||
|
})
|
||||||
|
.subscribe((result: any) => {
|
||||||
|
this.available = result.available;
|
||||||
|
this.unavailableMessage = result.message || '';
|
||||||
|
this.columns = result.columns || [];
|
||||||
|
this.rows = result.rows || [];
|
||||||
|
this.hasRun = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(): void {
|
||||||
|
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pm-container">
|
||||||
|
<div class="pm-header">
|
||||||
|
<button mat-stroked-button [routerLink]="['../']" class="pm-back">
|
||||||
|
<i class="fas fa-arrow-left"></i> Deposits
|
||||||
|
</button>
|
||||||
|
<h2 class="pm-header__title">Party Master Entry</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pm-card">
|
||||||
|
<form [formGroup]="partyForm">
|
||||||
|
<mat-tab-group class="pm-tabs" [(selectedIndex)]="selectedTabIndex" animationDuration="200ms">
|
||||||
|
<mat-tab label="Main Detail">
|
||||||
|
<div class="pm-tab-content">
|
||||||
|
<div class="pm-section">
|
||||||
|
<div class="pm-section__header">
|
||||||
|
<i class="fas fa-id-card"></i>
|
||||||
|
<h3>Identity</h3>
|
||||||
|
</div>
|
||||||
|
<div class="pm-grid">
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Client ID</mat-label>
|
||||||
|
<input matInput required formControlName="clientId" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Sub Type</mat-label>
|
||||||
|
<mat-select required formControlName="subType">
|
||||||
|
@for (type of subTypes; track type) {
|
||||||
|
<mat-option [value]="type">{{ type }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Agent ID</mat-label>
|
||||||
|
<input matInput required formControlName="agentId" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (linkedSavingsAccountId) {
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Savings Account</mat-label>
|
||||||
|
<input matInput [value]="'Linked (id ' + linkedSavingsAccountId + ')'" disabled />
|
||||||
|
</mat-form-field>
|
||||||
|
} @else {
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Savings Account No (if any)</mat-label>
|
||||||
|
<input matInput formControlName="savingsAccountNo" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Party Code</mat-label>
|
||||||
|
<input matInput formControlName="partyCode" placeholder="Auto-generated" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Last A/c</mat-label>
|
||||||
|
<input matInput formControlName="lastAccount" placeholder="—" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field">
|
||||||
|
<mat-label>Nominal</mat-label>
|
||||||
|
<mat-select required formControlName="nominal">
|
||||||
|
<mat-option value="Yes">Yes</mat-option>
|
||||||
|
<mat-option value="No">No</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
|
||||||
|
<mat-label>Party Name</mat-label>
|
||||||
|
<input matInput required formControlName="partyName" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="pm-field pm-field--wide">
|
||||||
|
<mat-label>Party Name 1</mat-label>
|
||||||
|
<input matInput formControlName="partyName1" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pm-section">
|
||||||
|
<div class="pm-section__header">
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
<h3>Address & 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 { OrganizationComponent } from './organization.component';
|
||||||
import { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.component';
|
import { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.component';
|
||||||
import { OfficesComponent } from './offices/offices.component';
|
import { OfficesComponent } from './offices/offices.component';
|
||||||
|
import { AgentMasterComponent } from './agent-master/agent-master.component';
|
||||||
|
import { CommissionSlabMasterComponent } from './commission-slab-master/commission-slab-master.component';
|
||||||
|
import { DdsBatchProcessingComponent } from './dds-batch-processing/dds-batch-processing.component';
|
||||||
import { EmployeesComponent } from './employees/employees.component';
|
import { EmployeesComponent } from './employees/employees.component';
|
||||||
import { CreateEmployeeComponent } from './employees/create-employee/create-employee.component';
|
import { CreateEmployeeComponent } from './employees/create-employee/create-employee.component';
|
||||||
import { ViewEmployeeComponent } from './employees/view-employee/view-employee.component';
|
import { ViewEmployeeComponent } from './employees/view-employee/view-employee.component';
|
||||||
@@ -237,6 +240,21 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'agent-master',
|
||||||
|
component: AgentMasterComponent,
|
||||||
|
data: { title: 'Agent Master', breadcrumb: 'Agent Master' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'commission-slab-master',
|
||||||
|
component: CommissionSlabMasterComponent,
|
||||||
|
data: { title: 'Commission Slab Master', breadcrumb: 'Commission Slab Master' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dds-batch-processing',
|
||||||
|
component: DdsBatchProcessingComponent,
|
||||||
|
data: { title: 'DDS Interest & Commission Batch', breadcrumb: 'DDS Batch Processing' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'employees',
|
path: 'employees',
|
||||||
data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' },
|
data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' },
|
||||||
|
|||||||
@@ -90,6 +90,47 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div #agentMaster>
|
||||||
|
<a class="org-card" [routerLink]="['agent-master']">
|
||||||
|
<div class="org-card__icon org-card__icon--teal">
|
||||||
|
<i class="fas fa-user-tie"></i>
|
||||||
|
</div>
|
||||||
|
<div class="org-card__body">
|
||||||
|
<h3 class="org-card__title">Agent Master</h3>
|
||||||
|
<p class="org-card__desc">
|
||||||
|
Register Daily Deposit Scheme collection agents, their commission/TDS settings, limits and guarantors
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-chevron-right org-card__arrow"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div #commissionSlabMaster>
|
||||||
|
<a class="org-card" [routerLink]="['commission-slab-master']">
|
||||||
|
<div class="org-card__icon org-card__icon--teal">
|
||||||
|
<i class="fas fa-percentage"></i>
|
||||||
|
</div>
|
||||||
|
<div class="org-card__body">
|
||||||
|
<h3 class="org-card__title">Commission Slab Master</h3>
|
||||||
|
<p class="org-card__desc">Define collection-volume and override commission percentage slabs</p>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-chevron-right org-card__arrow"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div #ddsBatchProcessing>
|
||||||
|
<a class="org-card" [routerLink]="['dds-batch-processing']">
|
||||||
|
<div class="org-card__icon org-card__icon--teal">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="org-card__body">
|
||||||
|
<h3 class="org-card__title">DDS Interest & Commission Batch</h3>
|
||||||
|
<p class="org-card__desc">Calculate then post interest, OD interest, and commission for DDS accounts</p>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-chevron-right org-card__arrow"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Manage Loan Originators -->
|
<!-- Manage Loan Originators -->
|
||||||
<div #manageLoanOriginators>
|
<div #manageLoanOriginators>
|
||||||
<a class="org-card" [routerLink]="['manage-loan-originators']" *mifosxHasPermission="'READ_LOAN_ORIGINATOR'">
|
<a class="org-card" [routerLink]="['manage-loan-originators']" *mifosxHasPermission="'READ_LOAN_ORIGINATOR'">
|
||||||
|
|||||||
@@ -233,6 +233,141 @@ export class OrganizationService {
|
|||||||
return this.http.get('/currencies');
|
return this.http.get('/currencies');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} DDS Agents data
|
||||||
|
*/
|
||||||
|
getAgents(): Observable<any> {
|
||||||
|
return this.http.get('/dds/agents');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} agentId Agent ID.
|
||||||
|
* @returns {Observable<any>} DDS Agent.
|
||||||
|
*/
|
||||||
|
getAgent(agentId: string | number): Observable<any> {
|
||||||
|
return this.http.get(`/dds/agents/${agentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} agent Agent to be created.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
createAgent(agent: any): Observable<any> {
|
||||||
|
return this.http.post('/dds/agents', agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} agentId Agent ID.
|
||||||
|
* @param {any} agent Agent changes to apply.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
updateAgent(agentId: string | number, agent: any): Observable<any> {
|
||||||
|
return this.http.put(`/dds/agents/${agentId}`, agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} DDS Parties data
|
||||||
|
*/
|
||||||
|
getParties(): Observable<any> {
|
||||||
|
return this.http.get('/dds/parties');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} partyId Party ID.
|
||||||
|
* @returns {Observable<any>} DDS Party.
|
||||||
|
*/
|
||||||
|
getParty(partyId: string | number): Observable<any> {
|
||||||
|
return this.http.get(`/dds/parties/${partyId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} party Party to be created.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
createParty(party: any): Observable<any> {
|
||||||
|
return this.http.post('/dds/parties', party);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} partyId Party ID.
|
||||||
|
* @param {any} party Party changes to apply.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
updateParty(partyId: string | number, party: any): Observable<any> {
|
||||||
|
return this.http.put(`/dds/parties/${partyId}`, party);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} partyId Party ID.
|
||||||
|
* @param {any} closeCommand { closeDate }
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
closeParty(partyId: string | number, closeCommand: any): Observable<any> {
|
||||||
|
return this.http.post(`/dds/parties/${partyId}?command=close`, closeCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} transaction Daily transaction batch (PARTY_TRAN collections or a WITHDRAW_DEPOSITE transfer) to post.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
createPartyTransaction(transaction: any): Observable<any> {
|
||||||
|
return this.http.post('/dds/partytransactions', transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} DDS Commission Slabs data
|
||||||
|
*/
|
||||||
|
getCommissionSlabs(): Observable<any> {
|
||||||
|
return this.http.get('/dds/commissionslabs');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any[]} slabs New commission slab rows to add to the chart.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
createCommissionSlabs(slabs: any[]): Observable<any> {
|
||||||
|
return this.http.post('/dds/commissionslabs', { slabs });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} batch Batch run filters (jobType, branchScope, jobDate, agentFrom/To, partyFrom/To) to generate a preview for.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
generateDdsBatchRun(batch: any): Observable<any> {
|
||||||
|
return this.http.post('/dds/batchruns', batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} batchRunId Batch run ID.
|
||||||
|
* @returns {Observable<any>} Batch run, including its previewed/posted rows.
|
||||||
|
*/
|
||||||
|
getDdsBatchRun(batchRunId: string | number): Observable<any> {
|
||||||
|
return this.http.get(`/dds/batchruns/${batchRunId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} batchRunId Batch run ID to post - applies exactly the rows it previewed.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
postDdsBatchRun(batchRunId: string | number): Observable<any> {
|
||||||
|
return this.http.post(`/dds/batchruns/${batchRunId}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} reportKey One of the backend DDS report keys.
|
||||||
|
* @param {any} filters fromDate, toDate (ISO strings), agentFrom/To, partyFrom/To.
|
||||||
|
* @returns {Observable<any>} { reportKey, available, message, columns, rows }
|
||||||
|
*/
|
||||||
|
runDdsReport(reportKey: string, filters: any): Observable<any> {
|
||||||
|
let httpParams = new HttpParams();
|
||||||
|
Object.keys(filters || {}).forEach((key) => {
|
||||||
|
if (filters[key] !== undefined && filters[key] !== null && filters[key] !== '') {
|
||||||
|
httpParams = httpParams.set(key, filters[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this.http.get(`/dds/reports/${reportKey}`, { params: httpParams });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any[]} currencies
|
* @param {any[]} currencies
|
||||||
* @returns {Observable<any>} Currencies data
|
* @returns {Observable<any>} Currencies data
|
||||||
|
|||||||
+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 { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component';
|
||||||
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
|
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
|
||||||
import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component';
|
import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component';
|
||||||
|
import { DailyDepositSchemeProductsComponent } from './daily-deposit-scheme-products/daily-deposit-scheme-products.component';
|
||||||
|
import { CreateDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component';
|
||||||
|
import { ViewDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component';
|
||||||
|
import { DailyDepositSchemeGeneralTabComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component';
|
||||||
|
import { DailyDepositSchemeDatatableTabComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component';
|
||||||
|
import { EditDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component';
|
||||||
import { ChargesComponent } from './charges/charges.component';
|
import { ChargesComponent } from './charges/charges.component';
|
||||||
import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
|
import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
|
||||||
import { CreateChargeComponent } from './charges/create-charge/create-charge.component';
|
import { CreateChargeComponent } from './charges/create-charge/create-charge.component';
|
||||||
@@ -75,6 +81,10 @@ import { ShareProductResolver } from './share-products/share-product-resolver';
|
|||||||
import { ShareProductAndTemplateResolver } from './share-products/edit-share-product/share-product-and-template.resolver';
|
import { ShareProductAndTemplateResolver } from './share-products/edit-share-product/share-product-and-template.resolver';
|
||||||
import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver';
|
import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver';
|
||||||
import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver';
|
import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver';
|
||||||
|
import { DailyDepositSchemeProductsResolver } from './daily-deposit-scheme-products/daily-deposit-scheme-products.resolver';
|
||||||
|
import { DailyDepositSchemeProductsTemplateResolver } from './daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver';
|
||||||
|
import { DailyDepositSchemeProductResolver } from './daily-deposit-scheme-products/daily-deposit-scheme-product.resolver';
|
||||||
|
import { DailyDepositSchemeProductAndTemplateResolver } from './daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver';
|
||||||
import { ChargesResolver } from './charges/charges.resolver';
|
import { ChargesResolver } from './charges/charges.resolver';
|
||||||
import { ChargeResolver } from './charges/charge.resolver';
|
import { ChargeResolver } from './charges/charge.resolver';
|
||||||
import { ChargesTemplateResolver } from './charges/charges-template.resolver';
|
import { ChargesTemplateResolver } from './charges/charges-template.resolver';
|
||||||
@@ -724,6 +734,83 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'daily-deposit-scheme-products',
|
||||||
|
data: { title: 'Daily Deposit Scheme Products', breadcrumb: 'Daily Deposit Scheme Products' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
component: CreateDailyDepositSchemeProductComponent,
|
||||||
|
data: { title: 'Create Daily Deposit Scheme Product', breadcrumb: 'Create' },
|
||||||
|
resolve: {
|
||||||
|
dailyDepositSchemeProductsTemplate: DailyDepositSchemeProductsTemplateResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: DailyDepositSchemeProductsComponent,
|
||||||
|
resolve: {
|
||||||
|
dailyDepositSchemeProducts: DailyDepositSchemeProductsResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':productId',
|
||||||
|
data: {
|
||||||
|
title: 'View Daily Deposit Scheme Product',
|
||||||
|
breadcrumb: 'productId',
|
||||||
|
routeParamBreadcrumb: 'productId'
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
recurringDepositDatatables: SavingProductDatatablesResolver
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ViewDailyDepositSchemeProductComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'general',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'general',
|
||||||
|
component: DailyDepositSchemeGeneralTabComponent,
|
||||||
|
resolve: {
|
||||||
|
dailyDepositSchemeProduct: DailyDepositSchemeProductResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'datatables',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':datatableName',
|
||||||
|
component: DailyDepositSchemeDatatableTabComponent,
|
||||||
|
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' },
|
||||||
|
resolve: {
|
||||||
|
recurringDepositDatatable: SavingProductDatatableResolver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
data: {
|
||||||
|
title: 'Edit Daily Deposit Scheme Product',
|
||||||
|
breadcrumb: 'edit',
|
||||||
|
routeParamBreadcrumb: false
|
||||||
|
},
|
||||||
|
component: EditDailyDepositSchemeProductComponent,
|
||||||
|
resolve: {
|
||||||
|
DailyDepositSchemeProductAndTemplate: DailyDepositSchemeProductAndTemplateResolver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'fixed-deposit-products',
|
path: 'fixed-deposit-products',
|
||||||
data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' },
|
data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' },
|
||||||
@@ -1004,6 +1091,10 @@ const routes: Routes = [
|
|||||||
ShareProductAndTemplateResolver,
|
ShareProductAndTemplateResolver,
|
||||||
RecurringDepositProductsResolver,
|
RecurringDepositProductsResolver,
|
||||||
RecurringDepositProductResolver,
|
RecurringDepositProductResolver,
|
||||||
|
DailyDepositSchemeProductsResolver,
|
||||||
|
DailyDepositSchemeProductsTemplateResolver,
|
||||||
|
DailyDepositSchemeProductResolver,
|
||||||
|
DailyDepositSchemeProductAndTemplateResolver,
|
||||||
ChargesResolver,
|
ChargesResolver,
|
||||||
ChargeResolver,
|
ChargeResolver,
|
||||||
ChargesTemplateAndResolver,
|
ChargesTemplateAndResolver,
|
||||||
|
|||||||
@@ -125,6 +125,25 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div #dailyDepositSchemeProducts>
|
||||||
|
<a
|
||||||
|
class="pd-card"
|
||||||
|
[routerLink]="['daily-deposit-scheme-products']"
|
||||||
|
*mifosxHasPermission="'READ_RECURRINGDEPOSITPRODUCT'"
|
||||||
|
>
|
||||||
|
<div class="pd-card__icon pd-card__icon--teal">
|
||||||
|
<i class="fas fa-calendar-day"></i>
|
||||||
|
</div>
|
||||||
|
<div class="pd-card__body">
|
||||||
|
<h3 class="pd-card__title">{{ 'labels.heading.Daily Deposit Scheme Products' | translate }}</h3>
|
||||||
|
<p class="pd-card__desc">
|
||||||
|
{{ 'labels.text.Add, modify or inactivate a Daily Deposit Scheme product' | translate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-chevron-right pd-card__arrow"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="pd-card" [routerLink]="['products-mix']" *mifosxHasPermission="'READ_PRODUCTMIX'">
|
<a class="pd-card" [routerLink]="['products-mix']" *mifosxHasPermission="'READ_PRODUCTMIX'">
|
||||||
<div class="pd-card__icon pd-card__icon--indigo">
|
<div class="pd-card__icon pd-card__icon--indigo">
|
||||||
<i class="fas fa-random"></i>
|
<i class="fas fa-random"></i>
|
||||||
|
|||||||
Reference in New Issue
Block a user