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,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();
});
}
}