DDS: Agent settlement (Agent Tran), Close Party account UI, reports UI, Party-Savings link field
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user