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,151 @@
<!--
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="cv-tab-content wri-tab">
<!-- ── TITLE BAR ── -->
<div class="wri-action-bar">
<div class="wri-action-bar__left">
<mat-icon class="wri-action-bar__icon">receipt_long</mat-icon>
<div>
<div class="wri-action-bar__title">Cheque Book Requests</div>
<div class="wri-action-bar__sub">
Series, stock receipt and book requisitions awaiting a second user's approval.
</div>
</div>
</div>
</div>
<!-- ── FILTER BAR ── -->
<div class="wri-filter-bar">
<select class="wri-filter-select" [(ngModel)]="filterStatus" (change)="onFilterChange()">
@for (opt of statusOptions; track opt.value) {
<option [value]="opt.value">{{ opt.label }}</option>
}
</select>
<span class="wri-fb-sep"></span>
<div class="wri-presets">
@for (p of datePresets; track p.key) {
<button
type="button"
class="wri-preset-chip"
[class.wri-preset-chip--active]="activePreset === p.key"
(click)="applyPreset(p.key)"
>
{{ p.label }}
</button>
}
</div>
<span class="wri-fb-sep"></span>
<div class="wri-date-range">
<div class="wri-date-box">
<span class="wri-date-box__lbl">From</span>
<input
class="wri-date-box__input"
[matDatepicker]="fromPicker"
[(ngModel)]="fromDate"
[max]="maxDate"
(dateChange)="onFromDateChange()"
readonly
/>
<button type="button" class="wri-date-box__btn" (click)="fromPicker.open()">
<mat-icon>event</mat-icon>
</button>
<mat-datepicker #fromPicker></mat-datepicker>
</div>
<span class="wri-date-range__sep">—</span>
<div class="wri-date-box">
<span class="wri-date-box__lbl">To</span>
<input
class="wri-date-box__input"
[matDatepicker]="toPicker"
[(ngModel)]="toDate"
[min]="fromDate"
[max]="maxDate"
(dateChange)="onToDateChange()"
readonly
/>
<button type="button" class="wri-date-box__btn" (click)="toPicker.open()">
<mat-icon>event</mat-icon>
</button>
<mat-datepicker #toPicker></mat-datepicker>
</div>
</div>
<button type="button" class="wri-btn wri-btn--refresh" (click)="load()" [disabled]="loading">
<mat-icon>refresh</mat-icon>
</button>
</div>
<!-- ── TABLE ── -->
@if (loading) {
<div class="empty-state">
<p class="empty-title">Loading…</p>
</div>
} @else if (loaded && requests.length === 0) {
<div class="empty-state">
<i class="fas fa-inbox empty-icon"></i>
<p class="empty-title">No {{ filterStatus | lowercase }} requests</p>
<p class="empty-text">
@if (filterStatus === 'PENDING') {
There are no pending cheque book requests waiting for review.
} @else {
No {{ filterStatus | lowercase }} cheque book requests found for this date range.
}
</p>
</div>
} @else {
<div class="wri-table-wrap">
<table class="wri-table">
<thead>
<tr>
<th class="wri-th-id">Id</th>
<th>Type</th>
<th>Maker</th>
<th>Submitted At</th>
<th>Status</th>
<th class="wri-th-action">Action</th>
</tr>
</thead>
<tbody>
@for (r of requests; track r.id) {
<tr class="wri-row" (click)="review(r)">
<td class="cell-mono wri-voucher">#{{ r.id }}</td>
<td>
<span class="wri-credit-badge">{{ requestTypeLabels[r.requestType] || r.requestType }}</span>
</td>
<td>{{ r.makerName || r.makerUsername }}</td>
<td class="wri-dt">{{ formatDateTime(r.submittedOn) }}</td>
<td>
<span class="wri-status" [ngClass]="statusClass(r.status)">{{ r.status }}</span>
</td>
<td class="wri-action-cell" (click)="$event.stopPropagation()">
<button type="button" class="wri-btn wri-btn--review" (click)="review(r)">
<mat-icon>rate_review</mat-icon>
Review
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<mat-paginator
[length]="totalRecords"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions"
[pageIndex]="pageIndex"
showFirstLastButtons
(page)="onPageChange($event)"
></mat-paginator>
}
</div>
@@ -0,0 +1,374 @@
/**
* 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/.
*/
:host {
display: block;
}
.wri-tab {
padding: 0;
}
.wri-action-bar {
display: flex;
align-items: center;
padding: 14px 20px;
background: #fff;
border-bottom: 1px solid #e2e8f0;
&__left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon {
font-size: 28px;
width: 28px;
height: 28px;
color: #0f4f3c;
}
&__title {
font-size: 15px;
font-weight: 700;
color: #0d1b2a;
}
&__sub {
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
}
.wri-filter-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
flex-wrap: wrap;
}
.wri-fb-sep {
width: 1px;
height: 22px;
background: #e2e8f0;
flex-shrink: 0;
margin: 0 4px;
}
.wri-filter-select {
height: 34px;
min-width: 130px;
padding: 0 10px;
border: 1px solid #e2e8f0;
border-radius: 7px;
font-size: 13px;
color: #0d1b2a;
font-family: inherit;
background: #fff;
cursor: pointer;
&:focus {
outline: none;
border-color: #0f4f3c;
}
}
.wri-presets {
display: inline-flex;
gap: 4px;
align-items: center;
}
.wri-preset-chip {
height: 28px;
padding: 0 12px;
border-radius: 14px;
border: 1px solid #e2e8f0;
background: #fff;
font-size: 12px;
font-weight: 600;
font-family: inherit;
color: #475569;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
&:hover {
border-color: #0f4f3c;
color: #0f4f3c;
background: #f0fdf4;
}
&--active {
background: #0f4f3c;
border-color: #0f4f3c;
color: #fff;
&:hover {
background: #0a3d2e;
}
}
}
.wri-date-range {
display: inline-flex;
align-items: center;
gap: 4px;
&__sep {
color: #94a3b8;
font-size: 16px;
font-weight: 300;
padding: 0 2px;
user-select: none;
}
}
.wri-date-box {
display: inline-flex;
align-items: center;
gap: 4px;
height: 34px;
padding: 0 6px 0 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
transition: border-color 0.15s;
&:hover {
border-color: #cbd5e1;
}
&:focus-within {
border-color: #0f4f3c;
}
&__lbl {
font-size: 11px;
font-weight: 600;
color: #94a3b8;
white-space: nowrap;
user-select: none;
letter-spacing: 0.03em;
}
&__input {
border: none;
outline: none;
font-size: 13px;
width: 76px;
color: #0d1b2a;
font-family: inherit;
background: transparent;
cursor: pointer;
}
&__btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border: none;
background: none;
cursor: pointer;
color: #94a3b8;
padding: 0;
border-radius: 4px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&:hover {
color: #0f4f3c;
background: #f0fdf4;
}
}
}
.wri-btn {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 7px;
font-family: inherit;
font-weight: 600;
cursor: pointer;
border: 1px solid #e2e8f0;
transition: background 0.15s;
height: 32px;
padding: 0 12px;
font-size: 13px;
background: #fff;
color: #0d1b2a;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--refresh {
padding: 0 8px;
color: #64748b;
&:hover:not(:disabled) {
background: #f1f5f9;
}
}
&--review {
background: #0f4f3c;
color: #fff;
border-color: #0f4f3c;
&:hover {
background: #0a3d2e;
}
}
}
.wri-table-wrap {
overflow-x: auto;
}
.wri-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th,
td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid #f1f5f9;
}
th {
font-size: 11px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.06em;
background: #f8fafc;
white-space: nowrap;
}
}
.wri-row {
cursor: pointer;
transition: background 0.1s;
&:hover td {
background: #f0fdf4;
}
}
.wri-th-id {
width: 60px;
}
.wri-th-action {
width: 110px;
text-align: center;
}
.wri-action-cell {
text-align: center;
}
.wri-dt {
font-size: 12px;
color: #64748b;
white-space: nowrap;
}
.wri-status {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
white-space: nowrap;
&--pending {
background: #fef3c7;
color: #92400e;
}
&--approved {
background: #dcfce7;
color: #166534;
}
&--rejected {
background: #fee2e2;
color: #991b1b;
}
}
.cell-mono {
font-family: 'Courier New', monospace;
font-size: 12px;
}
.wri-voucher {
font-weight: 700;
color: #1e40af;
letter-spacing: 0.06em;
}
.wri-credit-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 12px;
background: #e0f2fe;
color: #0369a1;
font-size: 11px;
font-weight: 600;
}
.empty-state {
text-align: center;
padding: 48px 24px;
color: #94a3b8;
.empty-icon {
font-size: 40px;
color: #cbd5e1;
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: #64748b;
margin: 12px 0 4px;
}
.empty-text {
font-size: 13px;
color: #94a3b8;
margin: 0;
}
}
@@ -0,0 +1,222 @@
/**
* 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 { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatDialog } from '@angular/material/dialog';
import { MatNativeDateModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatDatepickerModule } from '@angular/material/datepicker';
/** Custom Services */
import { ChequeBookService } from '../../../chequebook/chequebook.service';
import { SettingsService } from '../../../settings/settings.service';
/** Dialog Components */
import { ChequebookRequestDetailDialogComponent } from './request-detail-dialog/request-detail-dialog.component';
/**
* Bespoke maker-checker queue for cheque series / stock receipt / book requisition submissions — this
* CBS's own workflow (not Fineract's native command-approval engine, which bypasses maker-checker
* unconditionally for any role carrying ALL_FUNCTIONS, e.g. Super user). A DIFFERENT user than the maker
* must approve or reject each row here; the server enforces this even if the UI is bypassed.
*/
@Component({
selector: 'mifosx-chequebook-requests',
templateUrl: './chequebook-requests.component.html',
styleUrls: ['./chequebook-requests.component.scss'],
standalone: true,
imports: [
CommonModule,
FormsModule,
MatIconModule,
MatButtonModule,
MatPaginatorModule,
MatNativeDateModule,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule
]
})
export class ChequebookRequestsComponent implements OnInit {
private chequeBookService = inject(ChequeBookService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
requests: any[] = [];
totalRecords = 0;
pageSize = 10;
pageIndex = 0;
pageSizeOptions = [
10,
20,
50
];
filterStatus = 'PENDING';
fromDate: Date | null = null;
toDate: Date | null = null;
maxDate!: Date;
activePreset = 'all';
loading = false;
loaded = false;
readonly datePresets = [
{ key: 'today', label: 'Today' },
{ key: 'week', label: 'This Week' },
{ key: 'month', label: 'This Month' },
{ key: 'all', label: 'All' }
];
get statusOptions(): { value: string; label: string }[] {
return [
{ value: 'PENDING', label: 'Pending' },
{ value: 'APPROVED', label: 'Approved' },
{ value: 'REJECTED', label: 'Rejected' }
];
}
readonly requestTypeLabels: Record<string, string> = {
CREATE_SERIES: 'Add Cheque Series',
RECEIVE_STOCK: 'Receive Stock',
REQUEST_BOOK: 'Request Cheque Book',
DELETE_SERIES: 'Delete/Deactivate Cheque Series'
};
ngOnInit(): void {
const today = new Date(this.settingsService.businessDate);
today.setHours(0, 0, 0, 0);
this.maxDate = today;
this.applyPreset('all');
}
load(): void {
this.loading = true;
this.chequeBookService
.getPendingRequests(
this.filterStatus || undefined,
this.isoDate(this.fromDate),
this.isoDate(this.toDate),
this.pageIndex * this.pageSize,
this.pageSize
)
.subscribe({
next: (res: any) => {
this.requests = res?.pageItems ?? [];
this.totalRecords = Number(res?.totalFilteredRecords ?? 0);
this.loading = false;
this.loaded = true;
},
error: () => {
this.requests = [];
this.totalRecords = 0;
this.loading = false;
this.loaded = true;
}
});
}
private isoDate(d: Date | null): string | undefined {
if (!d) return undefined;
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
onPageChange(event: PageEvent): void {
this.pageSize = event.pageSize;
this.pageIndex = event.pageIndex;
this.load();
}
onFilterChange(): void {
this.pageIndex = 0;
this.load();
}
onFromDateChange(): void {
this.activePreset = '';
if (this.fromDate && this.toDate && this.fromDate > this.toDate) {
this.toDate = new Date(this.fromDate);
}
this.pageIndex = 0;
this.load();
}
onToDateChange(): void {
this.activePreset = '';
this.pageIndex = 0;
this.load();
}
applyPreset(key: string): void {
this.activePreset = key;
const today = new Date(this.settingsService.businessDate);
today.setHours(0, 0, 0, 0);
if (key === 'today') {
this.fromDate = new Date(today);
this.toDate = new Date(today);
} else if (key === 'week') {
const start = new Date(today);
start.setDate(today.getDate() - today.getDay());
this.fromDate = start;
this.toDate = new Date(today);
} else if (key === 'month') {
this.fromDate = new Date(today.getFullYear(), today.getMonth(), 1);
this.toDate = new Date(today);
} else {
this.fromDate = null;
this.toDate = null;
}
this.pageIndex = 0;
this.load();
}
review(request: any): void {
this.dialog
.open(ChequebookRequestDetailDialogComponent, { data: request, width: '640px' })
.afterClosed()
.subscribe((result: any) => {
if (result?.actioned) {
this.load();
}
});
}
statusClass(status: string): string {
const map: Record<string, string> = {
PENDING: 'wri-status--pending',
APPROVED: 'wri-status--approved',
REJECTED: 'wri-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);
}
}
}
@@ -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/.
-->
<div mat-dialog-title class="rdd-title">
<span class="rdd-id">#{{ data.id }}</span>
<span class="rdd-type-badge">{{ requestTypeLabels[data.requestType] || data.requestType }}</span>
<span class="rdd-spacer"></span>
<span class="rdd-maker">{{ data.makerName || data.makerUsername }}</span>
<span class="rdd-date">{{ formatDateTime(data.submittedOn) }}</span>
<span class="rdd-status" [ngClass]="'rdd-status--' + data.status.toLowerCase()">{{ data.status }}</span>
<button mat-raised-button color="primary" mat-dialog-close class="rdd-close-btn">
<mat-icon>expand_less</mat-icon>
Close
</button>
</div>
<div mat-dialog-content class="rdd-content">
<div class="rdd-payload">
@for (f of payloadEntries; track f.label) {
<div class="rdd-field">
<span class="k">{{ f.label }}</span>
<span>{{ f.value }}</span>
</div>
}
</div>
@if (data.status === 'REJECTED' && data.rejectionReason) {
<div class="rdd-reject-reason">
<span class="k">Rejection Reason</span>
<span>{{ data.rejectionReason }}</span>
</div>
}
@if (activityEntries.length > 0) {
<div class="rdd-activity">
<div class="k">Activity Log</div>
@for (a of activityEntries; track a.timestamp) {
<div class="rdd-activity-entry">
<span class="rdd-activity-action">{{ a.action }}</span>
<span class="rdd-activity-user">{{ a.user }}</span>
<span class="rdd-activity-time">{{ formatDateTime(a.timestamp) }}</span>
@if (a.note) {
<span class="rdd-activity-note">— {{ a.note }}</span>
}
</div>
}
</div>
}
</div>
@if (data.status === 'PENDING') {
<div mat-dialog-actions class="rdd-actions">
<button mat-raised-button color="primary" [disabled]="busy" (click)="approve()">Approve</button>
<button mat-stroked-button color="warn" [disabled]="busy" (click)="startReject()">Reject</button>
</div>
@if (rejecting) {
<form class="rdd-reject-form" [formGroup]="rejectForm" (ngSubmit)="submitReject()">
<mat-form-field appearance="fill">
<mat-label>Rejection Reason</mat-label>
<input matInput formControlName="reason" required />
</mat-form-field>
<button mat-raised-button color="warn" type="submit" [disabled]="rejectForm.invalid || busy">
Confirm Reject
</button>
</form>
}
}
@@ -0,0 +1,178 @@
/**
* 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/.
*/
.rdd-title {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
margin: 0 !important;
}
.rdd-id {
font-weight: 700;
color: #1e40af;
}
.rdd-type-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 12px;
background: #e0f2fe;
color: #0369a1;
font-size: 12px;
font-weight: 700;
}
.rdd-spacer {
flex: 1;
}
.rdd-maker {
font-size: 14px;
color: #0d1b2a;
}
.rdd-date {
font-size: 12px;
color: #64748b;
}
.rdd-status {
display: inline-flex;
align-items: center;
padding: 3px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
white-space: nowrap;
&--pending {
background: #fef3c7;
color: #92400e;
}
&--approved {
background: #dcfce7;
color: #166534;
}
&--rejected {
background: #fee2e2;
color: #991b1b;
}
}
.rdd-close-btn {
background: #0f4f3c !important;
color: #fff !important;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
margin-right: 2px;
}
}
.rdd-content {
min-width: 560px;
padding-top: 8px !important;
}
.rdd-payload {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px 24px;
.rdd-field {
display: flex;
flex-direction: column;
}
.k {
font-size: 0.7rem;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 0.04em;
}
}
.rdd-reject-reason {
display: flex;
flex-direction: column;
margin-top: 16px;
.k {
font-size: 0.7rem;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 0.04em;
}
}
.rdd-activity {
margin-top: 20px;
.k {
font-size: 0.7rem;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 0.04em;
margin-bottom: 8px;
}
}
.rdd-activity-entry {
display: flex;
gap: 10px;
font-size: 0.85rem;
padding: 5px 0;
border-bottom: 1px dashed #e2e8f0;
&:last-child {
border-bottom: none;
}
}
.rdd-activity-action {
font-weight: 600;
color: #0d1b2a;
}
.rdd-activity-user {
color: #64748b;
}
.rdd-activity-time {
color: #94a3b8;
}
.rdd-activity-note {
color: #64748b;
font-style: italic;
}
.rdd-actions {
display: flex;
gap: 12px;
}
.rdd-reject-form {
display: flex;
align-items: center;
gap: 12px;
padding: 0 24px 16px;
mat-form-field {
flex: 1;
max-width: 360px;
}
}
@@ -0,0 +1,170 @@
/**
* 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 {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBar } from '@angular/material/snack-bar';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { ChequeBookService } from '../../../../chequebook/chequebook.service';
/** Popup shown when a Cheque Book Request row's "Review" button is clicked. */
@Component({
selector: 'mifosx-chequebook-request-detail-dialog',
templateUrl: './request-detail-dialog.component.html',
styleUrls: ['./request-detail-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose,
MatIconModule,
MatButtonModule
]
})
export class ChequebookRequestDetailDialogComponent {
dialogRef = inject<MatDialogRef<ChequebookRequestDetailDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
private chequeBookService = inject(ChequeBookService);
private snackBar = inject(MatSnackBar);
private formBuilder = inject(UntypedFormBuilder);
private notify(message: string, isError = false): void {
this.snackBar.open(message, 'Dismiss', { duration: isError ? 6000 : 3000 });
}
busy = false;
rejecting = false;
rejectForm: UntypedFormGroup;
readonly requestTypeLabels: Record<string, string> = {
CREATE_SERIES: 'Add Cheque Series',
RECEIVE_STOCK: 'Receive Stock',
REQUEST_BOOK: 'Request Cheque Book',
DELETE_SERIES: 'Delete/Deactivate Cheque Series'
};
readonly fieldLabels: Record<string, string> = {
seriesCode: 'Series Code',
name: 'Name',
leavesPerBook: 'Leaves Per Book',
applicableAccountType: 'Applicable Account Type',
ctsCompliant: 'CTS-2010 Compliant',
reorderLevel: 'Reorder Level',
seriesId: 'Series',
officeId: 'Office',
rangeStart: 'Range Start',
rangeEnd: 'Range End',
batchRef: 'Batch Reference',
accountType: 'Account Type',
accountId: 'Account',
noOfBooks: 'Number of Books'
};
constructor() {
this.rejectForm = this.formBuilder.group({
reason: [
'',
Validators.required
]
});
}
get payloadEntries(): { label: string; value: any }[] {
try {
const payload = JSON.parse(this.data.payloadJson || '{}');
return Object.keys(payload)
.filter(
(key) => ![
'locale',
'dateFormat'
].includes(key)
)
.map((key) => ({ label: this.fieldLabels[key] || key, value: payload[key] }));
} catch {
return [];
}
}
get activityEntries(): { timestamp: string; action: string; user: string; note?: string }[] {
try {
return JSON.parse(this.data.activityLog || '[]');
} catch {
return [];
}
}
approve(): void {
if (this.busy) return;
this.busy = true;
this.chequeBookService.approvePendingRequest(this.data.id).subscribe({
next: () => {
this.busy = false;
this.notify('Request approved');
this.dialogRef.close({ actioned: true });
},
error: (err) => {
this.busy = false;
this.notify(err?.error?.defaultUserMessage || 'Approve failed', true);
}
});
}
startReject(): void {
this.rejecting = !this.rejecting;
this.rejectForm.reset();
}
submitReject(): void {
if (this.rejectForm.invalid || this.busy) return;
this.busy = true;
const payload = { reason: this.rejectForm.value.reason, locale: 'en' };
this.chequeBookService.rejectPendingRequest(this.data.id, payload).subscribe({
next: () => {
this.busy = false;
this.notify('Request rejected');
this.dialogRef.close({ actioned: true });
},
error: (err) => {
this.busy = false;
this.notify(err?.error?.defaultUserMessage || 'Reject failed', true);
}
});
}
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);
}
}
}
@@ -166,6 +166,18 @@
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Disbursement & Transfers</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./chequebook-requests']"
routerLinkActive
#chequebookRequests="routerLinkActive"
[active]="chequebookRequests.isActive"
*mifosxHasPermission="'READ_CHEQUEBOOKPENDINGREQUEST'"
>
<fa-icon icon="money-check" class="ac-tab__ic"></fa-icon>
<span>Cheque Book Requests</span>
</a>
</nav>
<mat-tab-nav-panel #tabPanel>
+6
View File
@@ -27,6 +27,7 @@ import { PendingAccountComponent } from './checker-inbox-and-tasks-tabs/pending-
import { RejectedAccountComponent } from './checker-inbox-and-tasks-tabs/rejected-account/rejected-account.component';
import { TerminatedAccountComponent } from './checker-inbox-and-tasks-tabs/terminated-account/terminated-account.component';
import { PendingSavingsProductsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component';
import { ChequebookRequestsComponent } from './checker-inbox-and-tasks-tabs/chequebook-requests/chequebook-requests.component';
import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inbox.component';
/** Custom Resolvers */
@@ -139,6 +140,11 @@ const routes: Routes = [
path: 'disbursement-requests',
component: DisbursementRequestsComponent,
data: { title: 'Disbursement Requests' }
},
{
path: 'chequebook-requests',
component: ChequebookRequestsComponent,
data: { title: 'Cheque Book Requests' }
}
]
},