Adds a rich decide dialog for clerks verifying inward CTS cheques: cheque images, all automatic validation results, and a mandatory signature-check gate built from the account's mode of operation and joint holders (backed by a new SignatoryService reusing the existing signature/document fetch pattern from the cashier module). Also adds a fullscreen compare mode: the cheque opens large with a shelf of every mandatory signer's stored signature, which can be dragged onto the cheque, resized, made semi-transparent, and zoomed, for a real side-by-side signature match instead of squinting at a thumbnail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
241 lines
9.8 KiB
HTML
241 lines
9.8 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.Inward Batch' | translate }} #{{ batch.id }}</h2>
|
||
<button mat-raised-button [routerLink]="['/ctsclearing', 'inward']">{{ 'labels.buttons.Back' | translate }}</button>
|
||
</div>
|
||
|
||
<mat-card class="info-card">
|
||
<mat-card-content>
|
||
<div class="info-grid">
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Batch Reference' | translate }}</span
|
||
><span>{{ batch.batchReference }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Session Date' | translate }}</span
|
||
><span>{{ batch.sessionDate ? (batch.sessionDate | dateFormat) : '–' }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Presentment Type' | translate }}</span
|
||
><span>{{ batch.presentmentType }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Items' | translate }}</span
|
||
><span>{{ batch.itemCount ?? batch.totalCount ?? items.length }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Total Amount' | translate }}</span
|
||
><span>{{ batch.totalAmount ?? '–' }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Status' | translate }}</span>
|
||
<span
|
||
><span class="pill" [ngClass]="pillClass(batch.status)">{{ batch.status }}</span></span
|
||
>
|
||
</div>
|
||
@if (batch.presentingBankName || batch.presentingBankCode) {
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Presenting Bank' | translate }}</span
|
||
><span
|
||
>{{ batch.presentingBankName || '–' }} <span class="muted">{{ batch.presentingBankCode }}</span></span
|
||
>
|
||
</div>
|
||
}
|
||
@if (batch.returnTatDeadline) {
|
||
<div>
|
||
<span class="k">{{ 'labels.inputs.Return TAT Deadline' | translate }}</span
|
||
><span>{{ batch.returnTatDeadline | dateFormat }}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</mat-card-content>
|
||
</mat-card>
|
||
|
||
<div class="action-toolbar">
|
||
<button mat-raised-button color="primary" (click)="runValidation()" [disabled]="busy">
|
||
<fa-icon icon="check-circle"></fa-icon> {{ 'labels.buttons.Run Validation' | translate }}
|
||
</button>
|
||
<button mat-raised-button (click)="prepareSettlement()" [disabled]="busy">
|
||
<fa-icon icon="money-check"></fa-icon> {{ 'labels.buttons.Prepare Settlement' | translate }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="table-wrapper mat-elevation-z1">
|
||
<table mat-table [dataSource]="items">
|
||
<ng-container matColumnDef="chequeNumber">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Cheque Number' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">
|
||
{{ it.chequeNumber }}
|
||
@if (it.instrumentStopPaymentFlag) {
|
||
<span class="pill bad" matTooltip="Stop payment was flagged on the depositing side">STOP</span>
|
||
}
|
||
@if (it.micrCode) {
|
||
<span class="muted">{{ it.micrCode }}</span>
|
||
}
|
||
@if (it.ifsc) {
|
||
<span class="muted">{{ it.ifsc }}</span>
|
||
}
|
||
@if (it.ctsReferenceNumber) {
|
||
<span class="muted">{{ it.ctsReferenceNumber }}</span>
|
||
}
|
||
@if (it.instrumentDate) {
|
||
<span class="muted">Dated {{ it.instrumentDate | dateFormat }}</span>
|
||
}
|
||
@if (it.instrumentTypeDescription || it.instrumentType) {
|
||
<span class="muted">{{ it.instrumentTypeDescription || it.instrumentType }}</span>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="drawer">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Account' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">
|
||
{{ it.accountNo || it.accountId || '–' }}
|
||
@if (it.clientName) {
|
||
<span class="muted">{{ it.clientName }}</span>
|
||
}
|
||
@if (it.drawerAccountNumberMasked) {
|
||
<span class="muted">Drawer a/c {{ it.drawerAccountNumberMasked }}</span>
|
||
}
|
||
@if (it.draweeBankName) {
|
||
<span class="muted">{{ it.draweeBankName }}</span>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="payeeName">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payee Name' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">{{ it.payeeName }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="amount">
|
||
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Amount' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it" class="num">
|
||
{{ it.amount }}
|
||
@if (it.amountInWords) {
|
||
<div class="muted">{{ it.amountInWords }}</div>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="image">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Image' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">
|
||
<div class="image-cell">
|
||
@if (getItemImageUrl(it, 'front')) {
|
||
<img
|
||
[src]="getItemImageUrl(it, 'front')"
|
||
class="cheque-thumb"
|
||
alt="front"
|
||
matTooltip="Front image — click to zoom"
|
||
(click)="openImageZoom(it, 'front')"
|
||
/>
|
||
} @else {
|
||
<input #frontFile type="file" accept="image/*" hidden (change)="uploadItemImage(it, 'front', $event)" />
|
||
<button mat-icon-button (click)="frontFile.click()" matTooltip="Upload front image">
|
||
<fa-icon icon="plus"></fa-icon>
|
||
</button>
|
||
}
|
||
@if (getItemImageUrl(it, 'back')) {
|
||
<img
|
||
[src]="getItemImageUrl(it, 'back')"
|
||
class="cheque-thumb"
|
||
alt="back"
|
||
matTooltip="Back image — click to zoom"
|
||
(click)="openImageZoom(it, 'back')"
|
||
/>
|
||
} @else {
|
||
<input #backFile type="file" accept="image/*" hidden (change)="uploadItemImage(it, 'back', $event)" />
|
||
<button mat-icon-button (click)="backFile.click()" matTooltip="Upload back image">
|
||
<fa-icon icon="plus"></fa-icon>
|
||
</button>
|
||
}
|
||
</div>
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="checks">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Validation' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">
|
||
<div class="check-badges">
|
||
@for (rule of rules; track rule.key) {
|
||
<span class="pill" [ngClass]="ruleClass(it, rule)" [title]="rule.label">{{ rule.label }}</span>
|
||
}
|
||
</div>
|
||
@if (it.validationDetail) {
|
||
<div class="validation-detail muted">{{ it.validationDetail }}</div>
|
||
}
|
||
@if (it.iqaStatus && it.iqaStatus !== 'NA') {
|
||
<span class="pill" [ngClass]="pillClass(it.iqaStatus)" matTooltip="Image Quality Assessment"
|
||
>IQA: {{ it.iqaStatus }}</span
|
||
>
|
||
}
|
||
@if (it.iqaDetail) {
|
||
<div class="muted">{{ it.iqaDetail }}</div>
|
||
}
|
||
@if (it.iqaSubChecks) {
|
||
<div class="muted">{{ it.iqaSubChecks }}</div>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="decision">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Decision' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">
|
||
@if (it.decision) {
|
||
<span class="pill" [ngClass]="pillClass(it.decision)">{{ it.decision }}</span>
|
||
} @else {
|
||
<span class="pill neutral">{{ it.itemStatus || 'PENDING' }}</span>
|
||
}
|
||
@if (it.returnReasonCode) {
|
||
<div class="muted">{{ it.returnReasonCode }}</div>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="hold">
|
||
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Hold' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it" class="num">
|
||
@if (it.holdPlaced) {
|
||
{{ it.holdAmount ?? '✓' }}
|
||
} @else {
|
||
–
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="returnDeadline">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Return Deadline' | translate }}</th>
|
||
<td mat-cell *matCellDef="let it">
|
||
{{ it.returnDeadline ? (it.returnDeadline | dateFormat) : '–' }}
|
||
@if (it.originalSerialNumber) {
|
||
<span class="muted">Orig # {{ it.originalSerialNumber }}</span>
|
||
}
|
||
@if (it.depositDate) {
|
||
<span class="muted">Deposited {{ it.depositDate | dateFormat }}</span>
|
||
}
|
||
@if (it.depositAccountNumber) {
|
||
<span class="muted">Deposit a/c {{ it.depositAccountNumber }}</span>
|
||
}
|
||
@if (it.presentingBranchCode) {
|
||
<span class="muted">Branch {{ it.presentingBranchCode }}</span>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="actions">
|
||
<th mat-header-cell *matHeaderCellDef></th>
|
||
<td mat-cell *matCellDef="let it">
|
||
<button mat-button color="primary" (click)="decide(it)">{{ 'labels.buttons.Decide' | translate }}</button>
|
||
</td>
|
||
</ng-container>
|
||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||
<tr mat-row *matRowDef="let row; columns: columns"></tr>
|
||
</table>
|
||
@if (items.length === 0) {
|
||
<div class="empty-state">{{ 'labels.text.No records to display' | translate }}</div>
|
||
}
|
||
</div>
|
||
|
||
<div #lightboxRoot style="display: none"></div>
|
||
</div>
|