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>
229 lines
8.2 KiB
TypeScript
229 lines
8.2 KiB
TypeScript
/**
|
|
* 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, Input, inject } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { Router } from '@angular/router';
|
|
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
|
import { ChequeBookService } from '../../chequebook/chequebook.service';
|
|
|
|
/** Read-only cheque-book view for the account currently loaded on the Clerk Dashboard —
|
|
* reached via the "Cheque Book" entry in the Account Facilities dropdown. */
|
|
@Component({
|
|
selector: 'mifosx-clerk-cheque-book',
|
|
standalone: true,
|
|
imports: [CommonModule],
|
|
styleUrl: './cheque-book.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</div>
|
|
@if (accountLoaded) {
|
|
<div class="cb-header__sub">
|
|
{{ accountNo }}
|
|
@if (clientName) {
|
|
· {{ clientName }}
|
|
}
|
|
@if (books.length > 0) {
|
|
· {{ books.length }} book{{ books.length === 1 ? '' : 's' }}
|
|
}
|
|
</div>
|
|
} @else {
|
|
<div class="cb-header__sub">View cheque books issued for the loaded account.</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
@if (!embedded) {
|
|
<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 to Dashboard
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
@if (!accountLoaded) {
|
|
<div class="cb-empty">
|
|
<div class="cb-empty__icon">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.3">
|
|
<rect x="1" y="4" width="22" height="16" rx="2" />
|
|
<line x1="1" y1="10" x2="23" y2="10" />
|
|
</svg>
|
|
</div>
|
|
<p class="cb-empty__title">No Account Loaded</p>
|
|
<p class="cb-empty__sub">Search an account on the Clerk Dashboard first.</p>
|
|
@if (!embedded) {
|
|
<button class="cb-empty__btn" (click)="goBack()">Go to Dashboard</button>
|
|
}
|
|
</div>
|
|
} @else {
|
|
@if (!facilityActive) {
|
|
<div class="cb-warn-banner">Cheque Book facility is not active for this account.</div>
|
|
}
|
|
|
|
@if (loading) {
|
|
<div class="cb-status-msg">Loading cheque books…</div>
|
|
} @else if (error) {
|
|
<div class="cb-status-msg cb-status-msg--error">{{ error }}</div>
|
|
} @else if (books.length === 0) {
|
|
<div class="cb-no-books">
|
|
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.3">
|
|
<rect x="1" y="4" width="22" height="16" rx="2" />
|
|
<line x1="1" y1="10" x2="23" y2="10" />
|
|
</svg>
|
|
<span>No cheque book issued for this account</span>
|
|
</div>
|
|
} @else {
|
|
<div class="cb-table-card">
|
|
<div class="cb-table-wrap">
|
|
<table class="cb-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Series</th>
|
|
<th>Books</th>
|
|
<th>Leaves</th>
|
|
<th>Status</th>
|
|
<th>Requested</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (b of books; track b.id) {
|
|
<tr class="cb-row" (click)="viewBook(b)">
|
|
<td>{{ b.seriesCode }}</td>
|
|
<td class="cb-cell--num">{{ b.noOfBooks }}</td>
|
|
<td class="cb-cell--num">{{ b.totalLeaves ?? '—' }}</td>
|
|
<td>
|
|
<span
|
|
class="cb-pill"
|
|
[class.cb-pill--ok]="b.status === 'ISSUED'"
|
|
[class.cb-pill--bad]="b.status === 'REJECTED' || b.status === 'WITHDRAWN'"
|
|
[class.cb-pill--neutral]="
|
|
b.status !== 'ISSUED' && b.status !== 'REJECTED' && b.status !== 'WITHDRAWN'
|
|
"
|
|
>{{ b.status }}</span
|
|
>
|
|
</td>
|
|
<td class="cb-cell--date">{{ b.requestedDate ?? '—' }}</td>
|
|
<td class="cb-cell--action">View ›</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
`
|
|
})
|
|
export class ClerkChequeBookComponent implements OnInit {
|
|
private clerkUiState = inject(ClerkUiStateService);
|
|
private chequeBookService = inject(ChequeBookService);
|
|
private router = inject(Router);
|
|
|
|
@Input() embedded = false;
|
|
|
|
accountLoaded = false;
|
|
accountNo = '';
|
|
clientName = '';
|
|
facilityActive = true;
|
|
books: any[] = [];
|
|
loading = false;
|
|
error = '';
|
|
|
|
ngOnInit(): void {
|
|
const s = this.clerkUiState.snapshot;
|
|
this.accountLoaded = s.accountLoaded;
|
|
if (!this.accountLoaded) {
|
|
return;
|
|
}
|
|
|
|
this.accountNo = s.savedAccount?.accountDetails?.accountNo ?? s.savedRow?.acNo ?? '';
|
|
const clientDetails = s.savedAccount?.clientDetails;
|
|
this.clientName = clientDetails ? `${clientDetails.firstname ?? ''} ${clientDetails.lastname ?? ''}`.trim() : '';
|
|
const facility = s.facilities.find((f) => f.label === 'Cheque Book');
|
|
this.facilityActive = facility ? facility.active : true;
|
|
|
|
const accountType = this.toChequeAccountType(s.savedRow?.acTyp);
|
|
const accountId = s.savedAccount?.accountDetails?.id;
|
|
if (accountId) {
|
|
this.loadBooks(accountType, accountId);
|
|
} else if (this.accountNo) {
|
|
this.resolveAccountId(accountType, this.accountNo);
|
|
}
|
|
}
|
|
|
|
/** Cheque-book series/books are only ever SAVINGS or CURRENT — CA/CB is the clerk's UI code for current accounts. */
|
|
private toChequeAccountType(acTyp?: string): string {
|
|
const t = (acTyp || '').toUpperCase();
|
|
return t === 'CA' || t === 'CB' ? 'CURRENT' : 'SAVINGS';
|
|
}
|
|
|
|
private resolveAccountId(accountType: string, accountNo: string): void {
|
|
this.loading = true;
|
|
this.chequeBookService.searchAccountByNo(accountType, accountNo).subscribe({
|
|
next: (res: any) => {
|
|
const items = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
|
const match = items.find((i: any) => i.accountNo === accountNo) ?? (items.length === 1 ? items[0] : null);
|
|
if (match) {
|
|
this.loadBooks(accountType, match.id);
|
|
} else {
|
|
this.loading = false;
|
|
this.error = 'Could not resolve this account for cheque book lookup';
|
|
}
|
|
},
|
|
error: () => {
|
|
this.loading = false;
|
|
this.error = 'Could not resolve this account for cheque book lookup';
|
|
}
|
|
});
|
|
}
|
|
|
|
private loadBooks(accountType: string, accountId: string | number): void {
|
|
this.loading = true;
|
|
this.chequeBookService.getBooks(accountType, accountId, undefined, undefined, 0, 50).subscribe({
|
|
next: (res: any) => {
|
|
this.books = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
|
this.loading = false;
|
|
},
|
|
error: () => {
|
|
this.loading = false;
|
|
this.error = 'Failed to load cheque book details';
|
|
}
|
|
});
|
|
}
|
|
|
|
viewBook(book: any): void {
|
|
this.router.navigate([
|
|
'/clerk/account-facilities/cheque-book',
|
|
book.id
|
|
]);
|
|
}
|
|
|
|
goBack(): void {
|
|
this.router.navigate(['/clerk/dashboard']);
|
|
}
|
|
}
|