feat(clerk): add clerk-facing Cheque Book screen and shell nav wiring

Adds a customer-account-scoped Cheque Book view under Clerk > Account
Facilities (list + leaf detail, with stop-payment/report-lost/mark-used/
return actions), reachable via a new hover dropdown on the Account
Facilities nav item. Also refreshes the Account Facilities page layout,
and carries over a few chequebook service/detail-dialog fixes from the
back-office module (used-leaf display, leaf-lookup dialog polish).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-15 16:08:43 +05:30
co-authored by Claude Sonnet 5
parent 7abfa394df
commit 0cc79ccd57
20 changed files with 1517 additions and 242 deletions
@@ -0,0 +1,207 @@
/**
* 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 { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { ChequeBookService } from '../../chequebook/chequebook.service';
import { LeafDetailDialogComponent } from '../../chequebook/cheque-book-detail/leaf-detail-dialog/leaf-detail-dialog.component';
import { ClerkLeafActionDialogComponent, LeafAction } from './leaf-action-dialog/leaf-action-dialog.component';
/** Cheque-book detail for the Clerk Dashboard — a fullscreen clerk-styled view of one book, its leaf
* register, and the same maker-side leaf actions (stop-payment, revoke, report-lost, cancel-leaf,
* mark-used) as the back-office ChequeBookDetailComponent, so a clerk can act on a leaf without leaving
* the clerk shell/nav. Every action still goes through the existing maker-checker approval flow server-side.
* Each action opens as a popup (ClerkLeafActionDialogComponent) rather than an inline form so it never
* pushes the leaf grid around and gets the full dialog width to lay out its fields. */
@Component({
selector: 'mifosx-clerk-cheque-book-detail',
standalone: true,
imports: [CommonModule],
styleUrl: './cheque-book-detail.component.scss',
template: `
<div class="cb-page">
<!-- Header -->
<div class="cb-header">
<div class="cb-header__left">
<svg
class="cb-header__icon"
width="26"
height="26"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
>
<rect x="1" y="4" width="22" height="16" rx="2" />
<line x1="1" y1="10" x2="23" y2="10" />
</svg>
<div>
<div class="cb-header__title">Cheque Book #{{ book.id }}</div>
<div class="cb-header__sub">
{{ book.accountNo || book.accountId }}
@if (book.clientName) {
&nbsp;&middot;&nbsp;{{ book.clientName }}
}
</div>
</div>
</div>
<button type="button" class="cb-btn" (click)="goBack()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="15 18 9 12 15 6" />
</svg>
Back
</button>
</div>
@if (loading) {
<div class="cb-status-msg">Loading cheque book&hellip;</div>
} @else {
<div class="cb-body">
<!-- Info card -->
<div class="cb-info-card">
<div class="cb-info-grid">
<div>
<span class="cb-info-grid__k">Account Type</span>
<span class="cb-info-grid__v">{{ book.accountType }}</span>
</div>
<div>
<span class="cb-info-grid__k">Office</span>
<span class="cb-info-grid__v">{{ book.officeName }}</span>
</div>
<div>
<span class="cb-info-grid__k">Series</span>
<span class="cb-info-grid__v">{{ book.seriesCode ?? '&#8212;' }}</span>
</div>
<div>
<span class="cb-info-grid__k">Leaf Range</span>
<span class="cb-info-grid__v">
{{ book.rangeStart ? book.rangeStart + ' – ' + book.rangeEnd : '&#8212;' }}
</span>
</div>
<div>
<span class="cb-info-grid__k">Quantity</span>
<span class="cb-info-grid__v">{{ book.noOfBooks }} book{{ book.noOfBooks === 1 ? '' : 's' }}</span>
</div>
<div>
<span class="cb-info-grid__k">Status</span>
<span
class="cb-pill"
[class.cb-pill--ok]="book.status === 'ISSUED'"
[class.cb-pill--bad]="book.status === 'REJECTED' || book.status === 'WITHDRAWN'"
[class.cb-pill--neutral]="
book.status !== 'ISSUED' && book.status !== 'REJECTED' && book.status !== 'WITHDRAWN'
"
>{{ book.status }}</span
>
</div>
<div>
<span class="cb-info-grid__k">Issue Date</span>
<span class="cb-info-grid__v">{{ book.issueDate ?? '&#8212;' }}</span>
</div>
</div>
</div>
<!-- Action toolbar — only for an issued book; each button pops up its own action dialog -->
@if (book.status === 'ISSUED') {
<div class="cb-action-toolbar">
<button type="button" class="cb-action-btn cb-action-btn--warn" (click)="openAction('stoppayment')">
Stop Payment
</button>
<button type="button" class="cb-action-btn" (click)="openAction('revokestoppayment')">
Revoke Stop Payment
</button>
<button type="button" class="cb-action-btn cb-action-btn--warn" (click)="openAction('reportlost')">
Report Lost
</button>
<button type="button" class="cb-action-btn" (click)="openAction('cancelleaf')">Cancel Leaf</button>
<button type="button" class="cb-action-btn cb-action-btn--primary" (click)="openAction('markused')">
Mark as Used
</button>
<button type="button" class="cb-action-btn cb-action-btn--warn" (click)="openAction('return')">
Flag Returned
</button>
</div>
}
<!-- Leaf grid -->
<div class="cb-info-card">
@if (leaves.length === 0) {
<div class="cb-no-books">No leaves to display</div>
} @else {
<div class="cb-leaf-grid">
@for (l of leaves; track l.leafSerialNo) {
<button
type="button"
class="cb-leaf-circle"
[class.cb-leaf-circle--ok]="l.status === 'ACTIVE'"
[class.cb-leaf-circle--used]="l.status === 'USED'"
[class.cb-leaf-circle--warn]="l.status === 'STOP_PAYMENT'"
[class.cb-leaf-circle--bad]="l.status === 'LOST'"
[class.cb-leaf-circle--returned]="l.status === 'RETURNED'"
(click)="selectLeaf(l)"
>
{{ l.leafSerialNo }}
</button>
}
</div>
}
</div>
</div>
}
</div>
`
})
export class ClerkChequeBookDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private chequeBookService = inject(ChequeBookService);
private dialog = inject(MatDialog);
bookId!: string;
book: any = {};
leaves: any[] = [];
loading = false;
ngOnInit(): void {
this.bookId = this.route.snapshot.paramMap.get('id') as string;
this.loadAll();
}
loadAll(): void {
this.loading = true;
this.chequeBookService.getBook(this.bookId).subscribe((data: any) => {
this.book = data || {};
this.loading = false;
});
this.chequeBookService.getLeaves(this.bookId).subscribe((data: any[]) => {
this.leaves = data || [];
});
}
selectLeaf(leaf: any): void {
this.dialog.open(LeafDetailDialogComponent, { data: leaf, width: '420px' });
}
openAction(action: LeafAction): void {
const ref = this.dialog.open(ClerkLeafActionDialogComponent, {
width: '480px',
data: { bookId: this.bookId, action }
});
ref.afterClosed().subscribe((result) => {
if (result?.success) {
this.loadAll();
}
});
}
goBack(): void {
this.router.navigate(['/clerk/account-facilities/cheque-book']);
}
}