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:
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
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="container">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.inputs.Leaf Lookup' | translate }}</h2>
|
||||
<button mat-raised-button [routerLink]="['/chequebook']">{{ 'labels.buttons.Back' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<mat-card class="search-card">
|
||||
<mat-card-content>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search()" class="search-row">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{ 'labels.inputs.Cheque Leaf Serial No' | translate }}</mat-label>
|
||||
<input matInput type="number" formControlName="serialNo" required />
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="searchForm.invalid || busy">
|
||||
<fa-icon icon="magnifying-glass"></fa-icon> {{ 'labels.buttons.Search' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@if (results.length > 1) {
|
||||
<div class="multi-match-note">
|
||||
{{
|
||||
'labels.text.More than one series has a leaf with this serial number — check the series code against the physical cheque.'
|
||||
| translate
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (result of results; track result.seriesCode + '-' + result.leafSerialNo) {
|
||||
<mat-card class="result-card">
|
||||
<mat-card-content>
|
||||
<div class="result-header">
|
||||
@if (result.seriesCode) {
|
||||
<span class="series-tag">{{ result.seriesCode }}</span>
|
||||
}
|
||||
<span
|
||||
class="pill big"
|
||||
[ngClass]="
|
||||
result.status === 'ACTIVE'
|
||||
? 'ok'
|
||||
: result.status === 'USED'
|
||||
? 'used'
|
||||
: result.status === 'STOP_PAYMENT'
|
||||
? 'warn'
|
||||
: result.status === 'LOST'
|
||||
? 'bad'
|
||||
: 'neutral'
|
||||
"
|
||||
>{{ result.status }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Serial No' | translate }}</span
|
||||
><span>{{ result.leafSerialNo }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Series' | translate }}</span
|
||||
><span>{{ result.seriesCode || '–' }}</span>
|
||||
</div>
|
||||
@if (result.chequeBookId) {
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Cheque Book' | translate }}</span
|
||||
><span>#{{ result.chequeBookId }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Account' | translate }}</span
|
||||
><span>{{ result.accountType }} {{ result.accountNo || result.accountId }}</span>
|
||||
</div>
|
||||
@if (result.clientName) {
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Client' | translate }}</span
|
||||
><span>{{ result.clientName }}</span>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="warn-note">
|
||||
{{
|
||||
'labels.text.This leaf was never issued to a customer — reported lost/stolen from stock.' | translate
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Office' | translate }}</span
|
||||
><span>{{ result.officeName }}</span>
|
||||
</div>
|
||||
@if (result.status === 'STOP_PAYMENT') {
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Stop Reason' | translate }}</span
|
||||
><span>{{ result.stopReason }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (result.status === 'LOST') {
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Lost Reason' | translate }}</span
|
||||
><span>{{ result.lostReason }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (result.status === 'CANCELLED') {
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Cancelled Reason' | translate }}</span
|
||||
><span>{{ result.cancelledReason }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (result.status === 'USED' && result.usedReason) {
|
||||
<div>
|
||||
<span class="k">{{ 'labels.inputs.Used Reason' | translate }}</span
|
||||
><span>{{ result.usedReason }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @empty {
|
||||
@if (searched && !busy) {
|
||||
<div class="empty-state">{{ 'labels.text.No records to display' | translate }}</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.container {
|
||||
padding: 16px;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.result-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.series-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
.multi-match-note {
|
||||
margin-bottom: 16px;
|
||||
color: #a16207;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px 24px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
.warn-note {
|
||||
grid-column: 1 / -1;
|
||||
color: #a16207;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
|
||||
&.big {
|
||||
font-size: 14px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.ok {
|
||||
background: #dff5e8;
|
||||
color: #28c07a;
|
||||
}
|
||||
|
||||
&.used {
|
||||
background: #e3edfd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.bad {
|
||||
background: #fde3e3;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
&.warn {
|
||||
background: #fff3cd;
|
||||
color: #a16207;
|
||||
}
|
||||
|
||||
&.neutral {
|
||||
background: #e0e0e0;
|
||||
color: #757575;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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, Validators } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Services */
|
||||
import { ChequeBookService } from '../chequebook.service';
|
||||
|
||||
/**
|
||||
* The teller/counter quick-lookup: given a bare cheque-leaf serial number, is it active, stopped, lost, or
|
||||
* was it ever issued at all — one code path covers every case (issued or unissued-but-stolen stock).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-leaf-lookup',
|
||||
templateUrl: './leaf-lookup.component.html',
|
||||
styleUrls: ['./leaf-lookup.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatSnackBarModule,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class LeafLookupComponent {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private chequeBookService = inject(ChequeBookService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
private notify(message: string, isError = false): void {
|
||||
this.snackBar.open(message, 'Dismiss', { duration: isError ? 6000 : 3000 });
|
||||
}
|
||||
|
||||
searchForm: UntypedFormGroup;
|
||||
// a bare serial number is only unique within its own series — more than one series can share a number,
|
||||
// so the lookup can legitimately return more than one match
|
||||
results: any[] = [];
|
||||
searched = false;
|
||||
busy = false;
|
||||
|
||||
constructor() {
|
||||
this.searchForm = this.formBuilder.group({
|
||||
serialNo: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
search(): void {
|
||||
if (this.searchForm.invalid || this.busy) {
|
||||
return;
|
||||
}
|
||||
this.busy = true;
|
||||
this.searched = true;
|
||||
this.results = [];
|
||||
this.chequeBookService.lookupLeaf(this.searchForm.value.serialNo).subscribe({
|
||||
next: (data: any) => {
|
||||
this.busy = false;
|
||||
this.results = Array.isArray(data) ? data : [data];
|
||||
},
|
||||
error: () => {
|
||||
this.busy = false;
|
||||
this.results = [];
|
||||
this.notify('No cheque leaf found with that serial number', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user