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.
240 lines
11 KiB
HTML
240 lines
11 KiB
HTML
<!--
|
|
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.Cheque Books' | translate }}</h2>
|
|
<div class="header-actions">
|
|
<button mat-raised-button color="primary" (click)="showRequestForm = !showRequestForm">
|
|
<fa-icon icon="plus"></fa-icon> {{ 'labels.buttons.Request Cheque Book' | translate }}
|
|
</button>
|
|
<button mat-raised-button [routerLink]="['/chequebook']">{{ 'labels.buttons.Back' | translate }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter-row">
|
|
@for (s of ['REQUESTED', 'ISSUED', 'REJECTED', 'WITHDRAWN']; track s) {
|
|
<button mat-stroked-button [class.active]="statusFilter === s" (click)="filterByStatus(s)">{{ s }}</button>
|
|
}
|
|
</div>
|
|
|
|
@if (showRequestForm) {
|
|
<mat-card class="add-form">
|
|
<mat-card-content>
|
|
<form [formGroup]="requestForm" (ngSubmit)="submitRequest()">
|
|
<div class="form-grid">
|
|
<mat-form-field appearance="fill">
|
|
<mat-label>{{ 'labels.inputs.Account Type' | translate }}</mat-label>
|
|
<mat-select formControlName="accountType" required (selectionChange)="onAccountTypeChange()">
|
|
<mat-option value="SAVINGS">{{ 'labels.inputs.Savings' | translate }}</mat-option>
|
|
<mat-option value="CURRENT">{{ 'labels.inputs.Current' | translate }}</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
<mat-form-field appearance="fill">
|
|
<mat-label>{{ 'labels.inputs.Account Number' | translate }}</mat-label>
|
|
<input
|
|
matInput
|
|
formControlName="accountNo"
|
|
(input)="onAccountNoInput($any($event.target).value)"
|
|
required
|
|
/>
|
|
@if (searchingAccount) {
|
|
<mat-hint>{{ 'labels.text.Searching...' | translate }}</mat-hint>
|
|
} @else if (resolvedAccount) {
|
|
<mat-hint class="ok-hint">
|
|
✓ {{ resolvedAccount.accountNo }}
|
|
@if (resolvedAccount.clientName) {
|
|
— {{ resolvedAccount.clientName }}
|
|
}
|
|
@if (resolvedAccount.status) {
|
|
({{ resolvedAccount.status }})
|
|
}
|
|
</mat-hint>
|
|
} @else if (accountError) {
|
|
<mat-hint class="err-hint">{{ accountError | translate }}</mat-hint>
|
|
} @else {
|
|
<mat-hint>{{ 'labels.text.Type at least 4 digits of the account number' | translate }}</mat-hint>
|
|
}
|
|
</mat-form-field>
|
|
<mat-form-field appearance="fill">
|
|
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
|
|
<mat-select formControlName="officeId" required>
|
|
@for (office of offices; track office.id) {
|
|
<mat-option [value]="office.id">{{ office.name }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
<mat-form-field appearance="fill">
|
|
<mat-label>{{ 'labels.inputs.Series' | translate }}</mat-label>
|
|
<mat-select formControlName="seriesId" required>
|
|
@for (s of requestableSeries; track s.id) {
|
|
<mat-option [value]="s.id">
|
|
{{ s.seriesCode }} — {{ s.name }} ({{ s.leavesPerBook }}-{{ 'labels.inputs.leaf' | translate }},
|
|
{{ availableBooksForRequestSeries(s.id) }} {{ 'labels.inputs.books available' | translate }})
|
|
</mat-option>
|
|
}
|
|
</mat-select>
|
|
<mat-hint>{{ 'labels.text.Only series applicable to this account type are shown' | translate }}</mat-hint>
|
|
</mat-form-field>
|
|
<mat-form-field appearance="fill">
|
|
<mat-label>{{ 'labels.inputs.Number of Books' | translate }}</mat-label>
|
|
<input matInput type="number" formControlName="noOfBooks" required />
|
|
<mat-hint>{{ 'labels.text.Leaves handed over = books x the series book size' | translate }}</mat-hint>
|
|
</mat-form-field>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button mat-raised-button type="button" (click)="showRequestForm = false">
|
|
{{ 'labels.buttons.Cancel' | translate }}
|
|
</button>
|
|
<button mat-raised-button color="primary" type="submit" [disabled]="requestForm.invalid || busy">
|
|
{{ 'labels.buttons.Submit' | translate }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</mat-card-content>
|
|
</mat-card>
|
|
}
|
|
|
|
<div class="table-wrapper mat-elevation-z1">
|
|
<table mat-table [dataSource]="books">
|
|
<ng-container matColumnDef="id">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Id' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">{{ b.id }}</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="accountType">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Account Type' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">{{ b.accountType }}</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="account">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Account' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">
|
|
{{ b.accountNo || b.accountId }}
|
|
@if (b.clientName) {
|
|
<span>({{ b.clientName }})</span>
|
|
}
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="officeName">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Office' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">{{ b.officeName }}</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="seriesCode">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Series' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">{{ b.seriesCode }}</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="quantity">
|
|
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Quantity' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b" class="num">
|
|
{{ b.noOfBooks }} {{ 'labels.inputs.books' | translate }}
|
|
@if (b.totalLeaves) {
|
|
<span class="muted">({{ b.totalLeaves }} {{ 'labels.inputs.leaves' | translate }})</span>
|
|
}
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="status">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Status' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">
|
|
<span
|
|
class="pill"
|
|
[ngClass]="
|
|
b.status === 'ISSUED' ? 'ok' : b.status === 'REJECTED' || b.status === 'WITHDRAWN' ? 'bad' : 'neutral'
|
|
"
|
|
>{{ b.status }}</span
|
|
>
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="requestedDate">
|
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Requested Date' | translate }}</th>
|
|
<td mat-cell *matCellDef="let b">{{ b.requestedDate | dateFormat }}</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="actions">
|
|
<th mat-header-cell *matHeaderCellDef></th>
|
|
<td mat-cell *matCellDef="let b" (click)="$event.stopPropagation()">
|
|
@if (b.status === 'REQUESTED') {
|
|
<button mat-button color="primary" (click)="toggleIssue(b)">
|
|
{{ 'labels.buttons.Issue' | translate }}
|
|
</button>
|
|
<button mat-button color="warn" (click)="toggleReject(b)">{{ 'labels.buttons.Reject' | translate }}</button>
|
|
}
|
|
@if (b.status === 'ISSUED') {
|
|
<button mat-button (click)="openBook(b)">{{ 'labels.buttons.View' | translate }}</button>
|
|
<button mat-button color="warn" (click)="withdraw(b)">{{ 'labels.buttons.Withdraw' | translate }}</button>
|
|
}
|
|
</td>
|
|
</ng-container>
|
|
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
|
<tr mat-row *matRowDef="let row; columns: columns" (click)="openBook(row)" class="clickable-row"></tr>
|
|
</table>
|
|
@if (books.length === 0) {
|
|
<div class="empty-state">{{ 'labels.text.No records to display' | translate }}</div>
|
|
}
|
|
@if (totalRecords > 0) {
|
|
<mat-paginator
|
|
[length]="totalRecords"
|
|
[pageSize]="pageSize"
|
|
[pageSizeOptions]="pageSizeOptions"
|
|
[pageIndex]="pageIndex"
|
|
showFirstLastButtons
|
|
(page)="onPageChange($event)"
|
|
></mat-paginator>
|
|
}
|
|
</div>
|
|
|
|
@if (issueForBookId) {
|
|
<mat-card class="add-form">
|
|
<mat-card-content>
|
|
<h3>{{ 'labels.inputs.Issue Cheque Book' | translate }}</h3>
|
|
<p class="issue-need">
|
|
{{ 'labels.inputs.Requested' | translate }}: <strong>{{ issueForBook?.noOfBooks }}</strong>
|
|
{{ 'labels.inputs.books' | translate }} — {{ issueForBook?.seriesCode }}
|
|
<span [class.err-hint]="issueAvailableBooks < (issueForBook?.noOfBooks || 0)">
|
|
({{ issueAvailableBooks }} {{ 'labels.inputs.books available' | translate }})
|
|
</span>
|
|
</p>
|
|
<form [formGroup]="issueForm" (ngSubmit)="submitIssue(issueForBookId)">
|
|
<mat-checkbox formControlName="collectionAcknowledged">
|
|
{{ 'labels.inputs.Customer collected the book now' | translate }}
|
|
</mat-checkbox>
|
|
<div class="form-actions">
|
|
<button mat-raised-button type="button" (click)="issueForBookId = null; issueForBook = null">
|
|
{{ 'labels.buttons.Cancel' | translate }}
|
|
</button>
|
|
<button mat-raised-button color="primary" type="submit" [disabled]="issueForm.invalid || busy">
|
|
{{ 'labels.buttons.Issue' | translate }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</mat-card-content>
|
|
</mat-card>
|
|
}
|
|
|
|
@if (rejectForBookId) {
|
|
<mat-card class="add-form">
|
|
<mat-card-content>
|
|
<h3>{{ 'labels.inputs.Reject Cheque Book Request' | translate }}</h3>
|
|
<form [formGroup]="rejectForm" (ngSubmit)="submitReject(rejectForBookId)">
|
|
<div class="form-grid">
|
|
<mat-form-field appearance="fill">
|
|
<mat-label>{{ 'labels.inputs.Rejection Reason' | translate }}</mat-label>
|
|
<input matInput formControlName="rejectionReason" required />
|
|
</mat-form-field>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button mat-raised-button type="button" (click)="rejectForBookId = null">
|
|
{{ 'labels.buttons.Cancel' | translate }}
|
|
</button>
|
|
<button mat-raised-button color="warn" type="submit" [disabled]="rejectForm.invalid || busy">
|
|
{{ 'labels.buttons.Reject' | translate }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</mat-card-content>
|
|
</mat-card>
|
|
}
|
|
</div>
|