feat(clerk): add GL disbursement maker-checker flow

- Add ClerkDisbursementService for submit/approve/reject HTTP calls
- Add DisbursementRequestsComponent (manager inbox tab under checker tasks)
- Add DisbursementReviewComponent (manager approve/reject detail page)
- Add MyDisbursementRequestsComponent (clerk's own disbursement history)
- Add 'My Disbursements' nav tab to clerk shell navigation header
- Wire routes in cashier-routing and tasks-routing modules
- Add 'My Disbursements' button to transaction-entry clerk footer
- Include accountName in creditEntries payload for manager review display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-16 10:43:29 +05:30
co-authored by Claude Sonnet 4.6
parent 89d1167145
commit efb01d5441
18 changed files with 2594 additions and 27 deletions
@@ -0,0 +1,147 @@
/**
* 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 } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatIconModule } from '@angular/material/icon';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ClerkDisbursementService } from '../../cashier/clerk-disbursement.service';
interface CreditEntry {
accountId: number;
accountNo: string;
accountName: string;
accountType: string;
amount: number;
}
@Component({
selector: 'mifosx-disbursement-review',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatSnackBarModule,
MatIconModule
],
templateUrl: './disbursement-review.component.html',
styleUrl: './disbursement-review.component.scss'
})
export class DisbursementReviewComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private service = inject(ClerkDisbursementService);
private snackBar = inject(MatSnackBar);
requestId: number | null = null;
request: any = null;
creditEntries: CreditEntry[] = [];
loading = false;
acting = false;
showRejectForm = false;
rejectionReason = '';
ngOnInit(): void {
const id = this.route.snapshot.params['id'];
this.requestId = id != null ? Number(id) : null;
if (this.requestId) this.loadRequest();
}
loadRequest(): void {
if (!this.requestId) return;
this.loading = true;
this.service
.getById(this.requestId)
.pipe(catchError(() => of(null)))
.subscribe((res: any) => {
this.request = res;
this.loading = false;
if (res?.credit_entries) {
try {
this.creditEntries =
typeof res.credit_entries === 'string' ? JSON.parse(res.credit_entries) : res.credit_entries;
} catch {
this.creditEntries = [];
}
}
});
}
approve(): void {
if (!this.requestId || this.acting) return;
this.acting = true;
this.service
.approve(this.requestId)
.pipe(catchError((err) => of({ error: err?.error?.errors?.[0]?.defaultUserMessage ?? 'Approval failed.' })))
.subscribe((res: any) => {
this.acting = false;
if (res?.error) {
this.snackBar.open(res.error, 'Close', { duration: 5000 });
} else {
this.snackBar.open('Disbursement approved successfully.', 'Close', { duration: 3000 });
this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']);
}
});
}
reject(): void {
if (!this.requestId || this.acting || !this.rejectionReason.trim()) return;
this.acting = true;
this.service
.reject(this.requestId, this.rejectionReason.trim())
.pipe(catchError((err) => of({ error: err?.error?.errors?.[0]?.defaultUserMessage ?? 'Rejection failed.' })))
.subscribe((res: any) => {
this.acting = false;
if (res?.error) {
this.snackBar.open(res.error, 'Close', { duration: 5000 });
} else {
this.snackBar.open('Request rejected.', 'Close', { duration: 3000 });
this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']);
}
});
}
totalCreditAmount(): number {
return this.creditEntries.reduce((sum, e) => sum + (e.amount ?? 0), 0);
}
statusClass(status: string): string {
const map: Record<string, string> = {
PENDING: 'dri-status--pending',
APPROVED: 'dri-status--approved',
REJECTED: 'dri-status--rejected'
};
return map[status] ?? '';
}
formatDateTime(val: any): string {
if (!val) return '—';
try {
const d = new Date(val);
return d.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return String(val);
}
}
backToInbox(): void {
this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']);
}
}