DDS: Agent settlement (Agent Tran), Close Party account UI, reports UI, Party-Savings link field

This commit is contained in:
tarunmalviya
2026-08-25 10:56:03 +05:30
parent 7ef75c902d
commit 57a61b3057
78 changed files with 12711 additions and 2 deletions
@@ -0,0 +1,355 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="am-container">
<div class="am-header">
<button mat-stroked-button [routerLink]="['../']" class="am-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
</button>
<h2 class="am-header__title">Agent Master Entry</h2>
</div>
<div class="am-card">
<form [formGroup]="agentForm">
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-id-card"></i>
<h3>Agent Details</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Client ID</mat-label>
<input matInput required formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Code</mat-label>
<input matInput formControlName="agentCode" placeholder="Auto-generated" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field am-field--wide">
<mat-label>Agent Name ID</mat-label>
<input matInput required formControlName="agentNameId" placeholder="Person or institution name" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field am-field--wide">
<mat-label>Agent Address (Line 1)</mat-label>
<input matInput required formControlName="agentAddress1" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field am-field--wide">
<mat-label>Agent Address (Line 2)</mat-label>
<input matInput formControlName="agentAddress2" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent City</mat-label>
<input matInput required formControlName="agentCity" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent PinCode</mat-label>
<input matInput required formControlName="agentPinCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Telephone</mat-label>
<input matInput formControlName="agentTelephone" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Home Branch</mat-label>
<input matInput required formControlName="homeBranch" />
</mat-form-field>
</div>
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-percent"></i>
<h3>Collection &amp; Commission Settings</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Com. Calculation</mat-label>
<mat-select required formControlName="commissionCalculation">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>TDS</mat-label>
<mat-select required formControlName="tdsApplicable">
<mat-option value="Yes">Yes</mat-option>
<mat-option value="No">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>TDS %</mat-label>
<input matInput type="number" formControlName="tdsPercent" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Cur.TDS</mat-label>
<input matInput formControlName="currentTds" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>TDS Amt</mat-label>
<input matInput formControlName="tdsAmount" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Limit</mat-label>
<input matInput type="number" formControlName="limit" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Machine Rent</mat-label>
<input matInput type="number" formControlName="machineRent" />
</mat-form-field>
</div>
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-piggy-bank"></i>
<h3>Settlement &amp; Security Accounts</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Saving Branch</mat-label>
<input matInput required formControlName="savingBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Saving A/C Type</mat-label>
<mat-select required formControlName="agentSavingAccountType">
@for (accountType of agentSavingAccountTypes; track accountType) {
<mat-option [value]="accountType">{{ accountType }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Saving A/C No</mat-label>
<input matInput required formControlName="agentSavingAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Trf Branch</mat-label>
<input matInput formControlName="transferBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Trf.Amt A/C Type</mat-label>
<input matInput formControlName="transferAmountAccountType" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Trf.Amt A/C No</mat-label>
<input matInput formControlName="transferAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Security Branch</mat-label>
<input matInput formControlName="agentSecurityBranch" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Security A/C Type</mat-label>
<mat-select formControlName="agentSecurityAccountType">
@for (accountType of agentSavingAccountTypes; track accountType) {
<mat-option [value]="accountType">{{ accountType }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Security A/C No</mat-label>
<input matInput formControlName="agentSecurityAccountNo" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Opening Date</mat-label>
<input matInput required [matDatepicker]="openingDatePicker" formControlName="agentOpeningDate" />
<mat-datepicker-toggle matIconSuffix [for]="openingDatePicker"></mat-datepicker-toggle>
<mat-datepicker #openingDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Birth Date</mat-label>
<input
matInput
required
[max]="maxDate"
[matDatepicker]="birthDatePicker"
formControlName="agentBirthDate"
/>
<mat-datepicker-toggle matIconSuffix [for]="birthDatePicker"></mat-datepicker-toggle>
<mat-datepicker #birthDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Current Balance</mat-label>
<input matInput formControlName="agentCurrentBalance" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Agent Opening Balance</mat-label>
<input matInput formControlName="agentOpeningBalance" />
</mat-form-field>
</div>
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-clock"></i>
<h3>Collection Limit History</h3>
<button mat-stroked-button type="button" class="am-add-guarantor" (click)="addLimit()">
<i class="fas fa-plus"></i> Add Limit
</button>
</div>
@if (limits.length === 0) {
<p class="am-empty-guarantors">No limit periods added yet.</p>
}
@for (limitRow of limits.controls; track limitRow; let i = $index) {
<div class="am-limit-row" [formGroup]="$any(limitRow)">
<mat-form-field appearance="outline" class="am-field">
<mat-label>From Date</mat-label>
<input matInput required [matDatepicker]="fromPicker" formControlName="fromDate" />
<mat-datepicker-toggle matIconSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>To Date</mat-label>
<input matInput required [matDatepicker]="toPicker" formControlName="toDate" />
<mat-datepicker-toggle matIconSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Limit</mat-label>
<input matInput required type="number" formControlName="limit" />
</mat-form-field>
<button
mat-icon-button
type="button"
class="am-remove-guarantor"
(click)="removeLimit(i)"
matTooltip="Remove limit period"
>
<i class="fas fa-trash"></i>
</button>
</div>
}
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-user-shield"></i>
<h3>Guarantor's Detail</h3>
<button mat-stroked-button type="button" class="am-add-guarantor" (click)="addGuarantor()">
<i class="fas fa-plus"></i> Add Guarantor
</button>
</div>
@if (guarantors.length === 0) {
<p class="am-empty-guarantors">No guarantors added yet.</p>
}
@for (guarantor of guarantors.controls; track guarantor; let i = $index) {
<div class="am-guarantor-row" [formGroup]="$any(guarantor)">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Client ID</mat-label>
<input matInput required formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Guarantor Name</mat-label>
<input matInput required formControlName="guarantorName" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Address 1</mat-label>
<input matInput required formControlName="address1" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Address 2</mat-label>
<input matInput formControlName="address2" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>City</mat-label>
<input matInput required formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Pin Code</mat-label>
<input matInput required formControlName="pinCode" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Telephone</mat-label>
<input matInput formControlName="telephone" />
</mat-form-field>
<button
mat-icon-button
type="button"
class="am-remove-guarantor"
(click)="removeGuarantor(i)"
matTooltip="Remove guarantor"
>
<i class="fas fa-trash"></i>
</button>
</div>
}
</div>
<div class="am-section">
<div class="am-section__header">
<i class="fas fa-wallet"></i>
<h3>Deductions &amp; Deposits</h3>
</div>
<div class="am-grid">
<mat-form-field appearance="outline" class="am-field">
<mat-label>Comm</mat-label>
<input matInput formControlName="commission" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Other Comm</mat-label>
<input matInput type="number" formControlName="otherCommission" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Ag.Deposit</mat-label>
<input matInput type="number" formControlName="agentDeposit" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Adv.Festival</mat-label>
<input matInput type="number" formControlName="festivalAdvance" />
</mat-form-field>
<mat-form-field appearance="outline" class="am-field">
<mat-label>Other Deduction</mat-label>
<input matInput type="number" formControlName="otherDeduction" />
</mat-form-field>
</div>
</div>
<div class="am-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
<button mat-stroked-button type="button" (click)="retrieve()">Retrieve</button>
<button mat-raised-button color="primary" type="button" [disabled]="agentForm.invalid" (click)="save()">
Save
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,141 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$primary: #0f766e;
.am-container {
max-width: 1040px;
margin: 0 auto;
padding: 24px 16px;
}
.am-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.am-back {
color: $primary;
}
.am-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 8px 24px 24px;
}
.am-section {
padding: 20px 0;
border-bottom: 1px solid rgb(0 0 0 / 8%);
&:last-of-type {
border-bottom: none;
}
&__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
i {
color: $primary;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
flex: 1;
}
}
}
.am-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.am-field {
width: 100%;
&--wide {
grid-column: span 2;
}
}
.am-add-guarantor {
color: $primary;
}
.am-empty-guarantors {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0 0 8px;
}
.am-guarantor-row {
display: grid;
grid-template-columns: repeat(7, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-top: 1px dashed rgb(0 0 0 / 10%);
&:first-of-type {
border-top: none;
}
}
.am-limit-row {
display: grid;
grid-template-columns: repeat(3, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-top: 1px dashed rgb(0 0 0 / 10%);
&:first-of-type {
border-top: none;
}
}
.am-remove-guarantor {
color: #b91c1c;
margin-top: 8px;
}
.am-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
flex-wrap: wrap;
}
@media (width <= 900px) {
.am-grid {
grid-template-columns: repeat(2, 1fr);
}
.am-guarantor-row,
.am-limit-row {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,381 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/**
* Agent Master Entry component.
*
* A DDS collection agent — either a person or an institution — registered
* with its own settlement savings account and one or more guarantors. As of
* 2026-08-24 the Agent also does the daily collection work directly (the
* SubAgent tier was removed): it carries the collection-limit history,
* TDS/commission settings and security account SubAgent used to own.
*
* Backed by its own DDS Agent API (`/dds/agents`) - independent of Staff/
* Client CRUD, per the DDS agent/commission hierarchy being genuinely new
* feature work.
*/
@Component({
selector: 'mifosx-agent-master',
templateUrl: './agent-master.component.html',
styleUrls: ['./agent-master.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class AgentMasterComponent {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
maxDate = new Date();
agentId: number | null = null;
agentSavingAccountTypes = [
'Savings Deposit Individual A/C',
'Savings Deposit Joint A/C',
'Current Account'
];
agentForm: UntypedFormGroup = this.formBuilder.group({
clientId: [
'',
Validators.required
],
agentCode: [
{
value: '',
disabled: true
}
],
agentNameId: [
'',
Validators.required
],
agentAddress1: [
'',
Validators.required
],
agentAddress2: [''],
agentCity: [
'',
Validators.required
],
agentPinCode: [
'',
Validators.required
],
agentTelephone: [''],
agentSavingAccountType: [
'',
Validators.required
],
agentSavingAccountNo: [
'',
Validators.required
],
agentOpeningDate: [
'',
Validators.required
],
agentBirthDate: [
'',
Validators.required
],
agentCurrentBalance: [
{
value: 0,
disabled: true
}
],
agentOpeningBalance: [
{
value: 0,
disabled: true
}
],
homeBranch: [
'',
Validators.required
],
tdsApplicable: [
'No',
Validators.required
],
tdsPercent: [0],
currentTds: [
{
value: 0,
disabled: true
}
],
tdsAmount: [
{
value: 0,
disabled: true
}
],
savingBranch: [
'',
Validators.required
],
transferBranch: [''],
transferAmountAccountType: [''],
transferAccountNo: [''],
commissionCalculation: [
'Yes',
Validators.required
],
limit: [0],
machineRent: [0],
agentSecurityBranch: [''],
agentSecurityAccountType: [''],
agentSecurityAccountNo: [''],
commission: [
{
value: 0,
disabled: true
}
],
otherCommission: [0],
agentDeposit: [0],
festivalAdvance: [0],
otherDeduction: [0],
limits: this.formBuilder.array([]),
guarantors: this.formBuilder.array([])
});
get limits(): UntypedFormArray {
return this.agentForm.get('limits') as UntypedFormArray;
}
get guarantors(): UntypedFormArray {
return this.agentForm.get('guarantors') as UntypedFormArray;
}
private buildLimitGroup(): UntypedFormGroup {
return this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
],
limit: [
0,
Validators.required
]
});
}
private buildGuarantorGroup(): UntypedFormGroup {
return this.formBuilder.group({
clientId: [
'',
Validators.required
],
guarantorName: [
'',
Validators.required
],
address1: [
'',
Validators.required
],
address2: [''],
city: [
'',
Validators.required
],
pinCode: [
'',
Validators.required
],
telephone: ['']
});
}
addLimit(): void {
this.limits.push(this.buildLimitGroup());
}
removeLimit(index: number): void {
this.limits.removeAt(index);
}
addGuarantor(): void {
this.guarantors.push(this.buildGuarantorGroup());
}
removeGuarantor(index: number): void {
this.guarantors.removeAt(index);
}
retrieve(): void {
const clientId = this.agentForm.get('clientId')?.value;
if (!clientId) {
return;
}
this.organizationService.getAgents().subscribe((agents: any[]) => {
const match = (agents || []).find((agent) => String(agent.clientId) === String(clientId));
if (match) {
this.patchFormFromAgent(match);
}
});
}
cancel(): void {
this.agentId = null;
this.agentForm.reset();
this.agentForm.patchValue({
agentCurrentBalance: 0,
agentOpeningBalance: 0,
tdsApplicable: 'No',
commissionCalculation: 'Yes',
currentTds: 0,
tdsAmount: 0,
commission: 0
});
this.limits.clear();
this.guarantors.clear();
}
exit(): void {
this.router.navigate(['/organization']);
}
save(): void {
const raw = this.agentForm.getRawValue();
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
const payload: any = {
clientId: Number(raw.clientId),
agentNameId: raw.agentNameId,
agentAddress1: raw.agentAddress1,
agentAddress2: raw.agentAddress2 || undefined,
agentCity: raw.agentCity,
agentPinCode: raw.agentPinCode,
agentTelephone: raw.agentTelephone || undefined,
agentSavingAccountType: raw.agentSavingAccountType,
agentSavingAccountNo: raw.agentSavingAccountNo,
agentOpeningDate: this.dateUtils.formatDate(raw.agentOpeningDate, dateFormat),
agentBirthDate: this.dateUtils.formatDate(raw.agentBirthDate, dateFormat),
agentOpeningBalance: raw.agentOpeningBalance || 0,
homeBranch: raw.homeBranch,
tdsApplicable: raw.tdsApplicable === 'Yes',
tdsPercent: raw.tdsPercent || undefined,
savingBranch: raw.savingBranch,
transferBranch: raw.transferBranch || undefined,
transferAmountAccountType: raw.transferAmountAccountType || undefined,
transferAccountNo: raw.transferAccountNo || undefined,
commissionCalculation: raw.commissionCalculation === 'Yes',
limit: raw.limit || undefined,
machineRent: raw.machineRent || undefined,
agentSecurityBranch: raw.agentSecurityBranch || undefined,
agentSecurityAccountType: raw.agentSecurityAccountType || undefined,
agentSecurityAccountNo: raw.agentSecurityAccountNo || undefined,
otherCommission: raw.otherCommission || undefined,
agentDeposit: raw.agentDeposit || undefined,
festivalAdvance: raw.festivalAdvance || undefined,
otherDeduction: raw.otherDeduction || undefined,
dateFormat,
locale,
limits: (raw.limits || []).map((limit: any) => ({
fromDate: this.dateUtils.getDate(limit.fromDate),
toDate: this.dateUtils.getDate(limit.toDate),
limit: limit.limit
})),
guarantors: (raw.guarantors || []).map((guarantor: any) => ({
clientId: Number(guarantor.clientId),
guarantorName: guarantor.guarantorName,
address1: guarantor.address1,
address2: guarantor.address2 || undefined,
city: guarantor.city,
pinCode: guarantor.pinCode,
telephone: guarantor.telephone || undefined
}))
};
if (this.agentId) {
this.organizationService.updateAgent(this.agentId, payload).subscribe(() => this.exit());
} else {
this.organizationService.createAgent(payload).subscribe(() => this.exit());
}
}
private patchFormFromAgent(agent: any): void {
this.agentId = agent.id;
this.agentForm.patchValue({
clientId: agent.clientId,
agentCode: agent.agentCode,
agentNameId: agent.agentNameId,
agentAddress1: agent.agentAddress1,
agentAddress2: agent.agentAddress2,
agentCity: agent.agentCity,
agentPinCode: agent.agentPinCode,
agentTelephone: agent.agentTelephone,
agentSavingAccountType: agent.agentSavingAccountType,
agentSavingAccountNo: agent.agentSavingAccountNo,
agentOpeningDate: agent.agentOpeningDate ? new Date(agent.agentOpeningDate) : null,
agentBirthDate: agent.agentBirthDate ? new Date(agent.agentBirthDate) : null,
agentCurrentBalance: agent.agentCurrentBalance,
agentOpeningBalance: agent.agentOpeningBalance,
homeBranch: agent.homeBranch,
tdsApplicable: agent.tdsApplicable ? 'Yes' : 'No',
tdsPercent: agent.tdsPercent,
currentTds: agent.currentTds,
tdsAmount: agent.tdsAmount,
savingBranch: agent.savingBranch,
transferBranch: agent.transferBranch,
transferAmountAccountType: agent.transferAmountAccountType,
transferAccountNo: agent.transferAccountNo,
commissionCalculation: agent.commissionCalculation ? 'Yes' : 'No',
limit: agent.limit,
machineRent: agent.machineRent,
agentSecurityBranch: agent.agentSecurityBranch,
agentSecurityAccountType: agent.agentSecurityAccountType,
agentSecurityAccountNo: agent.agentSecurityAccountNo,
commission: agent.commission,
otherCommission: agent.otherCommission,
agentDeposit: agent.agentDeposit,
festivalAdvance: agent.festivalAdvance,
otherDeduction: agent.otherDeduction
});
this.limits.clear();
(agent.limits || []).forEach((limit: any) => {
const group = this.buildLimitGroup();
group.patchValue({
fromDate: limit.fromDate ? new Date(limit.fromDate) : null,
toDate: limit.toDate ? new Date(limit.toDate) : null,
limit: limit.limit
});
this.limits.push(group);
});
this.guarantors.clear();
(agent.guarantors || []).forEach((guarantor: any) => {
const group = this.buildGuarantorGroup();
group.patchValue(guarantor);
this.guarantors.push(group);
});
}
}
@@ -0,0 +1,93 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="csm-container">
<div class="csm-header">
<button mat-stroked-button [routerLink]="['../']" class="csm-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
</button>
<h2 class="csm-header__title">Commission Slab Master</h2>
</div>
<div class="csm-card">
<table class="csm-table">
<thead>
<tr>
<th>Slab Type</th>
<th>Slab From</th>
<th>Slab To</th>
<th>Comm Per</th>
<th></th>
</tr>
</thead>
<tbody>
@for (slab of slabs; track slab; let i = $index) {
<tr>
<td>{{ slab.slabType }}</td>
<td>{{ slab.slabFrom }}</td>
<td>{{ slab.slabTo }}</td>
<td>{{ slab.commissionPercent }}%</td>
<td class="csm-row-actions">
<button mat-icon-button type="button" (click)="removeSlab(i)" matTooltip="Remove slab">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
<tr [formGroup]="newSlabForm" class="csm-new-row">
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<mat-select required formControlName="slabType" placeholder="Slab Type">
@for (type of slabTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
</td>
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<input matInput required type="number" formControlName="slabFrom" />
</mat-form-field>
</td>
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<input matInput required type="number" formControlName="slabTo" />
</mat-form-field>
</td>
<td>
<mat-form-field appearance="outline" class="csm-inline-field">
<input matInput required type="number" formControlName="commissionPercent" />
</mat-form-field>
</td>
<td class="csm-row-actions">
<button
mat-icon-button
color="primary"
type="button"
[disabled]="newSlabForm.invalid"
(click)="addSlab()"
matTooltip="Add slab"
>
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
</tbody>
</table>
@if (slabs.length === 0) {
<p class="csm-empty">No commission slabs defined yet — add one using the row above.</p>
}
<div class="csm-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-raised-button color="primary" type="button" (click)="save()">Save</button>
</div>
</div>
</div>
@@ -0,0 +1,92 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$primary: #0f766e;
.csm-container {
max-width: 760px;
margin: 0 auto;
padding: 24px 16px;
}
.csm-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.csm-back {
color: $primary;
}
.csm-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 16px 24px 24px;
}
.csm-table {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgb(0 0 0 / 55%);
padding: 8px 10px;
border-bottom: 2px solid rgb(0 0 0 / 10%);
}
td {
padding: 6px 10px;
border-bottom: 1px solid rgb(0 0 0 / 6%);
vertical-align: middle;
}
}
.csm-inline-field {
width: 100%;
::ng-deep .mat-mdc-text-field-wrapper {
height: 44px;
}
}
.csm-row-actions {
text-align: center;
width: 48px;
}
.csm-new-row {
background: rgb(15 118 110 / 4%);
}
.csm-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 12px 0 0;
}
.csm-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
@@ -0,0 +1,138 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
/**
* Commission Slab Master component.
*
* Two kinds of slab: ACCOUNT (commission percentage on collection volume)
* and COMMISSION (a senior-agent override on another agent's commission -
* deferred: there is no agent hierarchy modelled yet, see the DDS punch
* list). Modelled directly on the legacy Commission Slab Master screen this
* replaces.
*
* Backed by its own DDS Commission Slab API (`/dds/commissionslabs`) - an
* append-only chart, same as the legacy screen: existing rows load on open,
* newly-added rows post on Save, and there is no edit/delete of a
* previously-saved row.
*/
@Component({
selector: 'mifosx-commission-slab-master',
templateUrl: './commission-slab-master.component.html',
styleUrls: ['./commission-slab-master.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class CommissionSlabMasterComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private static readonly SLAB_TYPE_CODES: Record<string, string> = {
Account: 'ACCOUNT',
Commission: 'COMMISSION'
};
private static readonly SLAB_TYPE_LABELS: Record<string, string> = {
ACCOUNT: 'Account',
COMMISSION: 'Commission'
};
slabTypes = [
'Account',
'Commission'
];
displayedColumns: string[] = [
'slabType',
'slabFrom',
'slabTo',
'commissionPercent',
'actions'
];
slabs: Array<{ id?: number; slabType: string; slabFrom: number; slabTo: number; commissionPercent: number }> = [];
newSlabForm: UntypedFormGroup = this.formBuilder.group({
slabType: [
'',
Validators.required
],
slabFrom: [
0,
Validators.required
],
slabTo: [
0,
Validators.required
],
commissionPercent: [
0,
Validators.required
]
});
ngOnInit(): void {
this.loadSlabs();
}
private loadSlabs(): void {
this.organizationService.getCommissionSlabs().subscribe((slabs: any[]) => {
this.slabs = (slabs || []).map((slab) => ({
id: slab.id,
slabType: CommissionSlabMasterComponent.SLAB_TYPE_LABELS[slab.slabType] || slab.slabType,
slabFrom: slab.slabFrom,
slabTo: slab.slabTo,
commissionPercent: slab.commissionPercent
}));
});
}
addSlab(): void {
if (this.newSlabForm.invalid) {
return;
}
this.slabs.push(this.newSlabForm.getRawValue());
this.newSlabForm.reset({
slabType: '',
slabFrom: 0,
slabTo: 0,
commissionPercent: 0
});
}
removeSlab(index: number): void {
this.slabs.splice(index, 1);
}
exit(): void {
this.router.navigate(['/organization']);
}
save(): void {
const newSlabs = this.slabs.filter((slab) => !slab.id);
if (newSlabs.length === 0) {
return;
}
const payload = newSlabs.map((slab) => ({
slabType: CommissionSlabMasterComponent.SLAB_TYPE_CODES[slab.slabType] || slab.slabType,
slabFrom: slab.slabFrom,
slabTo: slab.slabTo,
commissionPercent: slab.commissionPercent
}));
this.organizationService.createCommissionSlabs(payload).subscribe(() => this.loadSlabs());
}
}
@@ -0,0 +1,133 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="bp-container">
<div class="bp-header">
<button mat-stroked-button [routerLink]="['../']" class="bp-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.heading.Organization' | translate }}
</button>
<h2 class="bp-header__title">DDS Interest &amp; Commission Batch Processing</h2>
</div>
<div class="bp-card">
<form [formGroup]="batchForm">
<div class="bp-grid">
<mat-form-field appearance="outline" class="bp-field bp-field--wide">
<mat-label>Job Type</mat-label>
<mat-select required formControlName="jobType">
@for (type of jobTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Branch</mat-label>
<mat-select required formControlName="branchScope">
@for (scope of branchScopes; track scope) {
<mat-option [value]="scope">{{ scope }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (isBranchSelected) {
<div class="bp-field bp-field--wide bp-branch-checklist">
@for (office of offices; track office.id) {
<mat-checkbox [checked]="selectedOfficeNames.has(office.name)" (change)="toggleOffice(office.name)">
{{ office.name }}
</mat-checkbox>
}
@if (offices.length === 0) {
<span class="bp-empty">Loading branches...</span>
}
</div>
}
<mat-form-field appearance="outline" class="bp-field">
<mat-label>{{ isPostingJob ? 'Posting Date' : 'As-On Date' }}</mat-label>
<input matInput required [matDatepicker]="jobDatePicker" formControlName="jobDate" />
<mat-datepicker-toggle matIconSuffix [for]="jobDatePicker"></mat-datepicker-toggle>
<mat-datepicker #jobDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Agent From</mat-label>
<input matInput required type="number" formControlName="agentFrom" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Agent To</mat-label>
<input matInput required type="number" formControlName="agentTo" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Party From</mat-label>
<input matInput required type="number" formControlName="partyFrom" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Party To</mat-label>
<input matInput required type="number" formControlName="partyTo" />
</mat-form-field>
</div>
<div class="bp-callout">
<i class="fas fa-info-circle"></i>
<span>
<strong>Generate</strong> runs a preview only — nothing is applied to any account. <strong>Post</strong> is a
separate, explicit step that commits the previewed amounts.
</span>
</div>
@if (hasGenerated) {
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-table"></i>
<h3>Preview</h3>
</div>
@if (previewRows.length === 0) {
<p class="bp-empty">
No calculation preview available yet — this appears once the backend batch-job endpoint for "{{
batchForm.get('jobType').value
}}" is connected.
</p>
} @else {
<table class="bp-table">
<thead>
<tr>
<th>Agent</th>
<th>Party</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
@for (row of previewRows; track row) {
<tr>
<td>{{ row.agent }}</td>
<td>{{ row.party }}</td>
<td>{{ row.amount | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
}
</div>
}
<div class="bp-action-bar">
<button mat-raised-button type="button" (click)="exit()">Exit</button>
<button mat-stroked-button type="button" [disabled]="batchForm.invalid" (click)="generate()">
<i class="fas fa-play" style="margin-right: 6px"></i> Generate
</button>
<button mat-raised-button color="primary" type="button" [disabled]="!hasGenerated" (click)="post()">
<i class="fas fa-check" style="margin-right: 6px"></i> Post
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,145 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$primary: #0f766e;
.bp-container {
max-width: 900px;
margin: 0 auto;
padding: 24px 16px;
}
.bp-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.bp-back {
color: $primary;
}
.bp-card {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
padding: 20px 24px 24px;
}
.bp-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.bp-field {
width: 100%;
&--wide {
grid-column: span 3;
}
}
.bp-branch-checklist {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
align-items: center;
padding: 8px 0;
}
.bp-callout {
display: flex;
gap: 10px;
align-items: flex-start;
background: rgb(15 118 110 / 6%);
border-left: 3px solid $primary;
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 16px 0;
font-size: 13px;
i {
color: $primary;
margin-top: 2px;
}
}
.bp-section {
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
i {
color: $primary;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
}
}
.bp-empty {
color: rgb(0 0 0 / 50%);
font-size: 14px;
margin: 0;
}
.bp-table {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgb(0 0 0 / 55%);
padding: 8px 10px;
border-bottom: 2px solid rgb(0 0 0 / 10%);
}
td {
padding: 8px 10px;
border-bottom: 1px solid rgb(0 0 0 / 6%);
}
}
.bp-action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgb(0 0 0 / 8%);
}
@media (width <= 900px) {
.bp-grid {
grid-template-columns: repeat(2, 1fr);
}
.bp-field--wide {
grid-column: span 2;
}
}
@@ -0,0 +1,186 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { OrganizationService } from 'app/organization/organization.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/**
* DDS Interest & Commission Batch Processing component.
*
* Six batch job types, each following the same deliberate two-step
* pattern from the legacy system: Calculate first (a dry-run preview),
* then Post as a separate, explicit confirmation — so a wrong calculation
* is never applied by accident. Modelled on the legacy Interest
* Calculation screen this replaces.
*
* Backed by the DDS Batch Run API (`/dds/batchruns`). Only the three
* "Calculation" job types can actually be generated - the three "Posting"
* entries exist in the dropdown for parity with the legacy menu, but Post
* always applies whatever run was most recently generated in this session,
* regardless of which dropdown entry is currently selected (matching how
* the Post button here is only ever gated on hasGenerated, not on jobType).
*/
@Component({
selector: 'mifosx-dds-batch-processing',
templateUrl: './dds-batch-processing.component.html',
styleUrls: ['./dds-batch-processing.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class DdsBatchProcessingComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private router = inject(Router);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private static readonly JOB_TYPE_CODES: Record<string, string> = {
'Interest Calculation': 'INTEREST_CALCULATION',
'OD Interest Calculation': 'OD_INTEREST_CALCULATION',
'Commission Calculation': 'COMMISSION_CALCULATION'
};
private currentBatchRunId: number | null = null;
offices: any[] = [];
selectedOfficeNames = new Set<string>();
jobTypes = [
'Interest Calculation',
'Interest Posting',
'OD Interest Calculation',
'OD Interest Posting',
'Commission Calculation',
'Commission Posting'
];
branchScopes = [
'All',
'Selected'
];
hasGenerated = false;
previewRows: Array<{ agent: string; party: string; amount: number }> = [];
batchForm: UntypedFormGroup = this.formBuilder.group({
jobType: [
'Interest Calculation',
Validators.required
],
branchScope: [
'All',
Validators.required
],
jobDate: [
new Date(),
Validators.required
],
agentFrom: [
1,
Validators.required
],
agentTo: [
99999,
Validators.required
],
partyFrom: [
1,
Validators.required
],
partyTo: [
99999999,
Validators.required
]
});
get isPostingJob(): boolean {
return (this.batchForm.get('jobType').value as string).includes('Posting');
}
get isBranchSelected(): boolean {
return this.batchForm.get('branchScope')?.value === 'Selected';
}
ngOnInit(): void {
this.organizationService.getOffices().subscribe((offices: any[]) => {
this.offices = offices || [];
});
}
toggleOffice(officeName: string): void {
if (this.selectedOfficeNames.has(officeName)) {
this.selectedOfficeNames.delete(officeName);
} else {
this.selectedOfficeNames.add(officeName);
}
}
generate(): void {
const raw = this.batchForm.getRawValue();
const jobType = DdsBatchProcessingComponent.JOB_TYPE_CODES[raw.jobType];
if (!jobType) {
// A "Posting" entry was selected - there is nothing to calculate for
// it, only to post (see class-level note on how Post is wired).
this.hasGenerated = true;
this.previewRows = [];
return;
}
const dateFormat = this.settingsService.dateFormat;
const payload: any = {
jobType,
branchScope: raw.branchScope,
jobDate: this.dateUtils.formatDate(raw.jobDate, dateFormat),
agentFrom: raw.agentFrom,
agentTo: raw.agentTo,
partyFrom: raw.partyFrom,
partyTo: raw.partyTo,
dateFormat,
locale: this.settingsService.language.code
};
if (raw.branchScope === 'Selected') {
payload.officeNames = Array.from(this.selectedOfficeNames);
}
this.organizationService.generateDdsBatchRun(payload).subscribe((result: any) => {
this.currentBatchRunId = result.resourceId;
this.organizationService.getDdsBatchRun(this.currentBatchRunId).subscribe((batchRun: any) => {
this.previewRows = (batchRun.rows || []).map((row: any) => ({
agent: row.agentCode,
party: row.partyCode || '—',
amount: row.amount
}));
this.hasGenerated = true;
});
});
}
post(): void {
if (!this.currentBatchRunId) {
return;
}
this.organizationService.postDdsBatchRun(this.currentBatchRunId).subscribe(() => {
this.currentBatchRunId = null;
this.hasGenerated = false;
this.previewRows = [];
});
}
exit(): void {
this.router.navigate(['/organization']);
}
}
@@ -17,6 +17,9 @@ import { Route } from '../core/route/route.service';
import { OrganizationComponent } from './organization.component';
import { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.component';
import { OfficesComponent } from './offices/offices.component';
import { AgentMasterComponent } from './agent-master/agent-master.component';
import { CommissionSlabMasterComponent } from './commission-slab-master/commission-slab-master.component';
import { DdsBatchProcessingComponent } from './dds-batch-processing/dds-batch-processing.component';
import { EmployeesComponent } from './employees/employees.component';
import { CreateEmployeeComponent } from './employees/create-employee/create-employee.component';
import { ViewEmployeeComponent } from './employees/view-employee/view-employee.component';
@@ -237,6 +240,21 @@ const routes: Routes = [
}
]
},
{
path: 'agent-master',
component: AgentMasterComponent,
data: { title: 'Agent Master', breadcrumb: 'Agent Master' }
},
{
path: 'commission-slab-master',
component: CommissionSlabMasterComponent,
data: { title: 'Commission Slab Master', breadcrumb: 'Commission Slab Master' }
},
{
path: 'dds-batch-processing',
component: DdsBatchProcessingComponent,
data: { title: 'DDS Interest & Commission Batch', breadcrumb: 'DDS Batch Processing' }
},
{
path: 'employees',
data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' },
@@ -90,6 +90,47 @@
</a>
</div>
<div #agentMaster>
<a class="org-card" [routerLink]="['agent-master']">
<div class="org-card__icon org-card__icon--teal">
<i class="fas fa-user-tie"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Agent Master</h3>
<p class="org-card__desc">
Register Daily Deposit Scheme collection agents, their commission/TDS settings, limits and guarantors
</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<div #commissionSlabMaster>
<a class="org-card" [routerLink]="['commission-slab-master']">
<div class="org-card__icon org-card__icon--teal">
<i class="fas fa-percentage"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Commission Slab Master</h3>
<p class="org-card__desc">Define collection-volume and override commission percentage slabs</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<div #ddsBatchProcessing>
<a class="org-card" [routerLink]="['dds-batch-processing']">
<div class="org-card__icon org-card__icon--teal">
<i class="fas fa-cogs"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">DDS Interest &amp; Commission Batch</h3>
<p class="org-card__desc">Calculate then post interest, OD interest, and commission for DDS accounts</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<!-- Manage Loan Originators -->
<div #manageLoanOriginators>
<a class="org-card" [routerLink]="['manage-loan-originators']" *mifosxHasPermission="'READ_LOAN_ORIGINATOR'">
@@ -233,6 +233,141 @@ export class OrganizationService {
return this.http.get('/currencies');
}
/**
* @returns {Observable<any>} DDS Agents data
*/
getAgents(): Observable<any> {
return this.http.get('/dds/agents');
}
/**
* @param {string | number} agentId Agent ID.
* @returns {Observable<any>} DDS Agent.
*/
getAgent(agentId: string | number): Observable<any> {
return this.http.get(`/dds/agents/${agentId}`);
}
/**
* @param {any} agent Agent to be created.
* @returns {Observable<any>}
*/
createAgent(agent: any): Observable<any> {
return this.http.post('/dds/agents', agent);
}
/**
* @param {string | number} agentId Agent ID.
* @param {any} agent Agent changes to apply.
* @returns {Observable<any>}
*/
updateAgent(agentId: string | number, agent: any): Observable<any> {
return this.http.put(`/dds/agents/${agentId}`, agent);
}
/**
* @returns {Observable<any>} DDS Parties data
*/
getParties(): Observable<any> {
return this.http.get('/dds/parties');
}
/**
* @param {string | number} partyId Party ID.
* @returns {Observable<any>} DDS Party.
*/
getParty(partyId: string | number): Observable<any> {
return this.http.get(`/dds/parties/${partyId}`);
}
/**
* @param {any} party Party to be created.
* @returns {Observable<any>}
*/
createParty(party: any): Observable<any> {
return this.http.post('/dds/parties', party);
}
/**
* @param {string | number} partyId Party ID.
* @param {any} party Party changes to apply.
* @returns {Observable<any>}
*/
updateParty(partyId: string | number, party: any): Observable<any> {
return this.http.put(`/dds/parties/${partyId}`, party);
}
/**
* @param {string | number} partyId Party ID.
* @param {any} closeCommand { closeDate }
* @returns {Observable<any>}
*/
closeParty(partyId: string | number, closeCommand: any): Observable<any> {
return this.http.post(`/dds/parties/${partyId}?command=close`, closeCommand);
}
/**
* @param {any} transaction Daily transaction batch (PARTY_TRAN collections or a WITHDRAW_DEPOSITE transfer) to post.
* @returns {Observable<any>}
*/
createPartyTransaction(transaction: any): Observable<any> {
return this.http.post('/dds/partytransactions', transaction);
}
/**
* @returns {Observable<any>} DDS Commission Slabs data
*/
getCommissionSlabs(): Observable<any> {
return this.http.get('/dds/commissionslabs');
}
/**
* @param {any[]} slabs New commission slab rows to add to the chart.
* @returns {Observable<any>}
*/
createCommissionSlabs(slabs: any[]): Observable<any> {
return this.http.post('/dds/commissionslabs', { slabs });
}
/**
* @param {any} batch Batch run filters (jobType, branchScope, jobDate, agentFrom/To, partyFrom/To) to generate a preview for.
* @returns {Observable<any>}
*/
generateDdsBatchRun(batch: any): Observable<any> {
return this.http.post('/dds/batchruns', batch);
}
/**
* @param {string | number} batchRunId Batch run ID.
* @returns {Observable<any>} Batch run, including its previewed/posted rows.
*/
getDdsBatchRun(batchRunId: string | number): Observable<any> {
return this.http.get(`/dds/batchruns/${batchRunId}`);
}
/**
* @param {string | number} batchRunId Batch run ID to post - applies exactly the rows it previewed.
* @returns {Observable<any>}
*/
postDdsBatchRun(batchRunId: string | number): Observable<any> {
return this.http.post(`/dds/batchruns/${batchRunId}`, {});
}
/**
* @param {string} reportKey One of the backend DDS report keys.
* @param {any} filters fromDate, toDate (ISO strings), agentFrom/To, partyFrom/To.
* @returns {Observable<any>} { reportKey, available, message, columns, rows }
*/
runDdsReport(reportKey: string, filters: any): Observable<any> {
let httpParams = new HttpParams();
Object.keys(filters || {}).forEach((key) => {
if (filters[key] !== undefined && filters[key] !== null && filters[key] !== '') {
httpParams = httpParams.set(key, filters[key]);
}
});
return this.http.get(`/dds/reports/${reportKey}`, { params: httpParams });
}
/**
* @param {any[]} currencies
* @returns {Observable<any>} Currencies data