Add customer search dialog and sponsor DD/reconciliation modules

Adds sponsor-dd and sponsor-dd-reconciliation feature modules, a
shared customer search dialog, and wires them into routing/shell/
organization screens alongside cashier and deposit-request updates.
This commit is contained in:
2026-07-22 11:59:31 +05:30
parent 1692925d0d
commit 3cf01e2733
36 changed files with 6257 additions and 18 deletions
@@ -0,0 +1,205 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { SponsorDDService } from '../sponsor-dd.service';
@Component({
selector: 'mifosx-gl-settings-dialog',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSnackBarModule
],
template: `
<h2 mat-dialog-title>GST / GL Settings</h2>
<mat-dialog-content>
<p class="hint-text">
GST is charged on the DD issuance commission/charges only, never on the DD face value. The rate below applies to
every DD submitted from now on - it's locked in per-request at submission time, so changing it here never
affects requests already in flight.
</p>
<form [formGroup]="form" class="dialog-form" *ngIf="!loading">
<mat-form-field appearance="outline" class="full-width">
<mat-label>GST Percentage (%)</mat-label>
<input matInput type="number" formControlName="gstPercentage" step="0.01" />
<mat-error *ngIf="form.get('gstPercentage')?.hasError('required')">Required</mat-error>
<mat-error *ngIf="form.get('gstPercentage')?.hasError('min') || form.get('gstPercentage')?.hasError('max')">
Must be between 0 and 100
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>DD Limit Amount</mat-label>
<input matInput type="number" formControlName="ddLimitAmount" step="0.01" />
<mat-hint>
Maximum face value for a single DD, and the maximum combined face value of all DDs issued to the same
beneficiary on the same day. Leave blank for no limit.
</mat-hint>
<mat-error *ngIf="form.get('ddLimitAmount')?.hasError('min')">Cannot be negative</mat-error>
</mat-form-field>
<div class="readonly-accounts">
<div class="account-row">
<span>DD Payable</span><strong>{{ mapping?.ddPayableGlAccountName || '-' }}</strong>
</div>
<div class="account-row">
<span>Cheque Clearing</span><strong>{{ mapping?.chequeClearingGlAccountName || '-' }}</strong>
</div>
<div class="account-row">
<span>Cash</span><strong>{{ mapping?.cashGlAccountName || '-' }}</strong>
</div>
<div class="account-row">
<span>Cash (Pending Approval)</span><strong>{{ mapping?.cashSuspenseGlAccountName || '-' }}</strong>
</div>
<div class="account-row">
<span>Charges Income</span><strong>{{ mapping?.chargesIncomeGlAccountName || '-' }}</strong>
</div>
<div class="account-row">
<span>GST Payable</span><strong>{{ mapping?.gstPayableGlAccountName || '-' }}</strong>
</div>
</div>
<p class="hint-text">
GL account assignments above are set up once by an administrator and shown here read-only.
</p>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onClose()">Close</button>
<button mat-flat-button color="primary" [disabled]="loading || form.invalid || saving" (click)="onSave()">
Save
</button>
</mat-dialog-actions>
`,
styles: [
`
.dialog-form {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 8px;
}
.full-width {
width: 100%;
}
.hint-text {
color: #64748b;
font-size: 13px;
margin: 0 0 12px 0;
}
.readonly-accounts {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px 16px;
background: #f8fafc;
}
.account-row {
display: flex;
justify-content: space-between;
font-size: 13px;
padding: 4px 0;
color: #334155;
}
.account-row span {
color: #64748b;
}
`
]
})
export class GlSettingsDialogComponent implements OnInit {
private fb = inject(FormBuilder);
private sponsorDDService = inject(SponsorDDService);
private dialogRef = inject(MatDialogRef<GlSettingsDialogComponent>);
private snackBar = inject(MatSnackBar);
form!: FormGroup;
loading = true;
saving = false;
mapping: any = null;
ngOnInit(): void {
this.form = this.fb.group({
gstPercentage: [
18,
[
Validators.required,
Validators.min(0),
Validators.max(100)
]
],
ddLimitAmount: [
null,
[Validators.min(0)]
]
});
this.sponsorDDService.getGlMappings().subscribe({
next: (mappings) => {
this.mapping = mappings && mappings.length ? mappings[0] : null;
if (this.mapping) {
this.form.patchValue({
gstPercentage: this.mapping.gstPercentage ?? 18,
ddLimitAmount: this.mapping.ddLimitAmount ?? null
});
}
this.loading = false;
},
error: (err) => {
this.loading = false;
this.snackBar.open('Error loading GL settings: ' + (err.error?.message || err.message), 'Close', {
duration: 5000
});
}
});
}
onSave(): void {
if (this.form.invalid || !this.mapping) return;
this.saving = true;
this.sponsorDDService
.upsertGlMapping({
sponsorBankId: 0,
ddPayableGlAccountId: this.mapping.ddPayableGlAccountId,
chequeClearingGlAccountId: this.mapping.chequeClearingGlAccountId,
cashGlAccountId: this.mapping.cashGlAccountId,
chargesIncomeGlAccountId: this.mapping.chargesIncomeGlAccountId,
cashSuspenseGlAccountId: this.mapping.cashSuspenseGlAccountId,
gstPercentage: this.form.value.gstPercentage,
gstPayableGlAccountId: this.mapping.gstPayableGlAccountId,
ddLimitAmount: this.form.value.ddLimitAmount
})
.subscribe({
next: () => {
this.snackBar.open('GST percentage updated', 'Close', { duration: 3000 });
this.dialogRef.close(true);
},
error: (err) => {
this.saving = false;
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 5000 });
}
});
}
onClose(): void {
this.dialogRef.close();
}
}