feat(chequebook): add cheque book module with Maker-Checker workflow

Adds cheque series creation, stock receive, cheque leaf request/lookup,
and stop-payment/lost-reporting flows with Maker-Checker approval and
a checker-inbox tab for pending chequebook requests.
This commit is contained in:
Jaanu423
2026-07-15 11:02:46 +05:30
parent b9856a1c93
commit 7abfa394df
40 changed files with 4892 additions and 2 deletions
@@ -0,0 +1,135 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { Observable } from 'rxjs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { ChequeBookService } from '../chequebook.service';
import { LeafDetailDialogComponent } from './leaf-detail-dialog/leaf-detail-dialog.component';
type LeafAction = 'stoppayment' | 'revokestoppayment' | 'reportlost' | 'cancelleaf' | 'markused';
/**
* One issued cheque book: its leaf register, and the five leaf-range actions (stop-payment, revoke,
* report-lost, cancel-leaf, mark-used) — each defaults to the whole book when no range is given.
* "Mark as used" is a manual record (this CBS has no clearing/presentment integration to detect it itself)
* — a branch sets it once it sees a leaf clear on the bank/clearing statement.
*/
@Component({
selector: 'mifosx-cheque-book-detail',
templateUrl: './cheque-book-detail.component.html',
styleUrls: ['./cheque-book-detail.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
RouterLink,
MatButtonModule,
MatSnackBarModule,
FaIconComponent
]
})
export class ChequeBookDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private formBuilder = inject(UntypedFormBuilder);
private chequeBookService = inject(ChequeBookService);
private snackBar = inject(MatSnackBar);
private dialog = inject(MatDialog);
private notify(message: string, isError = false): void {
this.snackBar.open(message, 'Dismiss', { duration: isError ? 6000 : 3000 });
}
bookId!: string;
book: any = {};
leaves: any[] = [];
busy = false;
activeAction: LeafAction | null = null;
actionForm: UntypedFormGroup;
constructor() {
this.actionForm = this.formBuilder.group({
fromLeafSeq: [''],
toLeafSeq: [''],
reason: ['']
});
}
ngOnInit(): void {
this.bookId = this.route.snapshot.paramMap.get('id') as string;
this.loadAll();
}
loadAll(): void {
this.chequeBookService.getBook(this.bookId).subscribe((data: any) => (this.book = data || {}));
this.chequeBookService.getLeaves(this.bookId).subscribe((data: any[]) => {
this.leaves = data || [];
});
}
selectLeaf(leaf: any): void {
this.dialog.open(LeafDetailDialogComponent, { data: leaf, width: '420px' });
}
startAction(action: LeafAction): void {
this.activeAction = this.activeAction === action ? null : action;
this.actionForm.reset();
const reasonControl = this.actionForm.get('reason');
if (action === 'revokestoppayment' || action === 'markused') {
reasonControl?.clearValidators();
} else {
reasonControl?.setValidators(Validators.required);
}
reasonControl?.updateValueAndValidity();
}
submitAction(): void {
if (this.actionForm.invalid || this.busy || !this.activeAction) {
return;
}
this.busy = true;
const raw = this.actionForm.value;
const payload: any = { locale: 'en' };
if (raw.fromLeafSeq !== '' && raw.fromLeafSeq != null) payload.fromLeafSeq = raw.fromLeafSeq;
if (raw.toLeafSeq !== '' && raw.toLeafSeq != null) payload.toLeafSeq = raw.toLeafSeq;
if (raw.reason) payload.reason = raw.reason;
const actionCalls: Record<LeafAction, (bookId: string, payload: any) => Observable<any>> = {
stoppayment: (id, p) => this.chequeBookService.stopPayment(id, p),
revokestoppayment: (id, p) => this.chequeBookService.revokeStopPayment(id, p),
reportlost: (id, p) => this.chequeBookService.reportBookLost(id, p),
cancelleaf: (id, p) => this.chequeBookService.cancelLeaf(id, p),
markused: (id, p) => this.chequeBookService.markUsed(id, p)
};
const call = actionCalls[this.activeAction](this.bookId, payload);
call.subscribe({
next: () => {
this.busy = false;
this.activeAction = null;
this.notify('Action applied');
this.loadAll();
},
error: () => {
this.busy = false;
this.notify(
'Action failed — check the leaf range and that the leaves are in a state that allows this action',
true
);
}
});
}
}