Receive Stock form now displays the actual highest-allocated serial across every series (not just silently prefilling the field), so the operator can see and trust the auto-suggested range start rather than taking it on faith. Fixes a real bug where item.decision (which InwardClearingItem.java always sets to the literal string "PENDING" at creation, never null) was checked with a plain truthy test everywhere a "has this been decided yet" branch was needed. Since "PENDING" is a non-empty string, that check was always true — silently breaking the Decide/View button label, the collapsed validation summary, the "Awaiting settlement" text, and the decision dialog itself (which never showed the actual Pass/Return form, even for still-pending items). Replaced with a proper isDecided() check everywhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
340 lines
16 KiB
HTML
340 lines
16 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 Stock' | translate }}</h2>
|
||
<button mat-raised-button [routerLink]="['/chequebook']">{{ 'labels.buttons.Back' | translate }}</button>
|
||
</div>
|
||
|
||
<mat-tab-group>
|
||
<!-- ============ Stock Lots ============ -->
|
||
<mat-tab label="{{ 'labels.inputs.Stock Lots' | translate }}">
|
||
<div class="tab-body">
|
||
<!-- Denomination-wise availability roll-up: how many 25-leaf / 50-leaf books are on hand -->
|
||
@if (stockSummary.length > 0) {
|
||
<div class="summary-strip">
|
||
<div class="summary-title">{{ 'labels.inputs.Available Stock by Book Size' | translate }}</div>
|
||
<div class="summary-cards">
|
||
@for (row of stockSummary; track row.seriesCode + row.leavesPerBook) {
|
||
<div class="summary-card">
|
||
<div class="summary-books">{{ row.books }}</div>
|
||
<div class="summary-label">
|
||
{{ 'labels.inputs.books' | translate }} × {{ row.leavesPerBook }}-{{
|
||
'labels.inputs.leaf' | translate
|
||
}}
|
||
</div>
|
||
<div class="summary-series">{{ row.seriesCode }}</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<div class="tab-toolbar">
|
||
<button mat-raised-button color="primary" (click)="showReceiveForm = !showReceiveForm">
|
||
<fa-icon icon="plus"></fa-icon> {{ 'labels.buttons.Receive Stock' | translate }}
|
||
</button>
|
||
</div>
|
||
|
||
@if (showReceiveForm) {
|
||
<mat-card class="add-form">
|
||
<mat-card-content>
|
||
<div class="global-allocation-note">
|
||
{{ 'labels.text.Highest leaf serial allocated so far, across every series' | translate }}:
|
||
<strong>{{ globalHighestAllocated }}</strong>
|
||
— {{ 'labels.text.next available start' | translate }}:
|
||
<strong>{{ globalHighestAllocated + 1 }}</strong>
|
||
</div>
|
||
<form [formGroup]="receiveForm" (ngSubmit)="submitReceive()">
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.Series' | translate }}</mat-label>
|
||
<mat-select formControlName="seriesId" required (selectionChange)="onReceiveSeriesChange()">
|
||
@for (s of seriesList; track s.id) {
|
||
<mat-option [value]="s.id"
|
||
>{{ s.seriesCode }} — {{ s.name }} ({{ s.leavesPerBook }}-{{
|
||
'labels.inputs.leaf' | translate
|
||
}})</mat-option
|
||
>
|
||
}
|
||
</mat-select>
|
||
</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.Number of Books' | translate }}</mat-label>
|
||
<input matInput type="number" formControlName="noOfBooks" required />
|
||
@if (receiveLeavesPerBook) {
|
||
<mat-hint
|
||
>{{ receiveTotalLeaves }} {{ 'labels.inputs.leaves' | translate }} ({{
|
||
'labels.inputs.books' | translate
|
||
}}
|
||
× {{ receiveLeavesPerBook }})</mat-hint
|
||
>
|
||
}
|
||
</mat-form-field>
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.Range Start' | translate }}</mat-label>
|
||
<input matInput type="number" formControlName="rangeStart" required />
|
||
<mat-hint
|
||
>{{ 'labels.text.Auto-suggested to avoid overlap across every series; editable' | translate }} ({{
|
||
globalHighestAllocated
|
||
}}
|
||
+ 1)</mat-hint
|
||
>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.Range End' | translate }}</mat-label>
|
||
<input matInput type="number" [value]="receiveRangeEnd ?? ''" readonly />
|
||
<mat-hint>{{ 'labels.text.Calculated from books x book size' | translate }}</mat-hint>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.Batch / PO Reference' | translate }}</mat-label>
|
||
<input matInput formControlName="batchRef" />
|
||
</mat-form-field>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button mat-raised-button type="button" (click)="showReceiveForm = false">
|
||
{{ 'labels.buttons.Cancel' | translate }}
|
||
</button>
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="submit"
|
||
[disabled]="receiveForm.invalid || !receiveRangeEnd || busy"
|
||
>
|
||
{{ 'labels.buttons.Submit' | translate }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</mat-card-content>
|
||
</mat-card>
|
||
}
|
||
|
||
<div class="filter-row">
|
||
@for (s of lotStatusOptions; track s) {
|
||
<button mat-stroked-button [class.active]="lotStatusFilter === s" (click)="filterLotsByStatus(s)">
|
||
{{ s }}
|
||
</button>
|
||
}
|
||
</div>
|
||
|
||
<div class="table-wrapper mat-elevation-z1">
|
||
<table mat-table [dataSource]="displayedLots">
|
||
<ng-container matColumnDef="seriesCode">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Series' | translate }}</th>
|
||
<td mat-cell *matCellDef="let l">{{ l.seriesCode }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="officeName">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Office' | translate }}</th>
|
||
<td mat-cell *matCellDef="let l">{{ l.officeName }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="range">
|
||
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Leaf Range' | translate }}</th>
|
||
<td mat-cell *matCellDef="let l" class="num">{{ l.rangeStart }} – {{ l.rangeEnd }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="remainingLeaves">
|
||
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Remaining' | translate }}</th>
|
||
<td mat-cell *matCellDef="let l" class="num">
|
||
<strong>{{ l.remainingBooks ?? 0 }}</strong> {{ 'labels.inputs.books' | translate }}
|
||
<span class="muted">× {{ l.leavesPerBook }}-{{ 'labels.inputs.leaf' | translate }}</span>
|
||
<span class="muted">({{ l.remainingLeaves }} {{ 'labels.inputs.leaves' | translate }})</span>
|
||
@if (l.lowStock) {
|
||
<span class="pill warn">{{ 'labels.text.Low Stock' | translate }}</span>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="status">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Status' | translate }}</th>
|
||
<td mat-cell *matCellDef="let l">
|
||
<span
|
||
class="pill"
|
||
[ngClass]="l.status === 'AVAILABLE' ? 'ok' : l.status === 'CANCELLED' ? 'bad' : 'neutral'"
|
||
>{{ l.status }}</span
|
||
>
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="actions">
|
||
<th mat-header-cell *matHeaderCellDef></th>
|
||
<td mat-cell *matCellDef="let l">
|
||
@if (l.status === 'AVAILABLE') {
|
||
<button mat-button color="warn" (click)="openReportLost(l)">
|
||
{{ 'labels.buttons.Report Lost' | translate }}
|
||
</button>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<tr mat-header-row *matHeaderRowDef="lotColumns"></tr>
|
||
<tr mat-row *matRowDef="let row; columns: lotColumns"></tr>
|
||
</table>
|
||
@if (displayedLots.length === 0) {
|
||
<div class="empty-state">{{ 'labels.text.No records to display' | translate }}</div>
|
||
}
|
||
@if (lotTotalRecords > 0) {
|
||
<mat-paginator
|
||
[length]="lotTotalRecords"
|
||
[pageSize]="lotPageSize"
|
||
[pageSizeOptions]="pageSizeOptions"
|
||
[pageIndex]="lotPageIndex"
|
||
showFirstLastButtons
|
||
(page)="onLotPageChange($event)"
|
||
></mat-paginator>
|
||
}
|
||
</div>
|
||
</div>
|
||
</mat-tab>
|
||
|
||
<!-- ============ Stock Transfers ============ -->
|
||
<mat-tab label="{{ 'labels.inputs.Stock Transfers' | translate }}">
|
||
<div class="tab-body">
|
||
<div class="tab-toolbar">
|
||
<button mat-raised-button color="primary" (click)="showDispatchForm = !showDispatchForm">
|
||
<fa-icon icon="plus"></fa-icon> {{ 'labels.buttons.Dispatch Transfer' | translate }}
|
||
</button>
|
||
</div>
|
||
|
||
@if (showDispatchForm) {
|
||
<mat-card class="add-form">
|
||
<mat-card-content>
|
||
<form [formGroup]="dispatchForm" (ngSubmit)="submitDispatch()">
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.Series' | translate }}</mat-label>
|
||
<mat-select formControlName="seriesId" required>
|
||
@for (s of seriesList; track s.id) {
|
||
<mat-option [value]="s.id">{{ s.seriesCode }} — {{ s.name }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.From Office' | translate }}</mat-label>
|
||
<mat-select formControlName="fromOfficeId" 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.To Office' | translate }}</mat-label>
|
||
<mat-select formControlName="toOfficeId" 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.From Lot Id' | translate }}</mat-label>
|
||
<input matInput type="number" formControlName="fromLotId" required />
|
||
<mat-hint>{{
|
||
'labels.text.The stock-lot id to draw the transfer from (see Stock Lots tab)' | translate
|
||
}}</mat-hint>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="fill">
|
||
<mat-label>{{ 'labels.inputs.No of Leaves' | translate }}</mat-label>
|
||
<input matInput type="number" formControlName="noOfLeaves" required />
|
||
</mat-form-field>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button mat-raised-button type="button" (click)="showDispatchForm = false">
|
||
{{ 'labels.buttons.Cancel' | translate }}
|
||
</button>
|
||
<button mat-raised-button color="primary" type="submit" [disabled]="dispatchForm.invalid || busy">
|
||
{{ 'labels.buttons.Submit' | translate }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</mat-card-content>
|
||
</mat-card>
|
||
}
|
||
|
||
<div class="filter-row">
|
||
@for (s of transferStatusOptions; track s) {
|
||
<button mat-stroked-button [class.active]="transferStatusFilter === s" (click)="filterTransfersByStatus(s)">
|
||
{{ s }}
|
||
</button>
|
||
}
|
||
</div>
|
||
|
||
<div class="table-wrapper mat-elevation-z1">
|
||
<table mat-table [dataSource]="stockTransfers">
|
||
<ng-container matColumnDef="seriesCode">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Series' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t">{{ t.seriesCode }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="fromOfficeName">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.From Office' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t">{{ t.fromOfficeName }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="toOfficeName">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.To Office' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t">{{ t.toOfficeName }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="range">
|
||
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Leaf Range' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t" class="num">{{ t.rangeStart }} – {{ t.rangeEnd }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="totalLeaves">
|
||
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Leaves' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t" class="num">{{ t.totalLeaves }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="status">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Status' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t">
|
||
<span
|
||
class="pill"
|
||
[ngClass]="t.status === 'RECEIVED' ? 'ok' : t.status === 'REJECTED' ? 'bad' : 'neutral'"
|
||
>{{ t.status }}</span
|
||
>
|
||
</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="dispatchDate">
|
||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Dispatch Date' | translate }}</th>
|
||
<td mat-cell *matCellDef="let t">{{ t.dispatchDate | dateFormat }}</td>
|
||
</ng-container>
|
||
<ng-container matColumnDef="actions">
|
||
<th mat-header-cell *matHeaderCellDef></th>
|
||
<td mat-cell *matCellDef="let t">
|
||
@if (t.status === 'DISPATCHED') {
|
||
<button mat-button color="primary" (click)="receiveTransfer(t)">
|
||
{{ 'labels.buttons.Receive' | translate }}
|
||
</button>
|
||
<button mat-button color="warn" (click)="rejectTransfer(t)">
|
||
{{ 'labels.buttons.Reject' | translate }}
|
||
</button>
|
||
}
|
||
</td>
|
||
</ng-container>
|
||
<tr mat-header-row *matHeaderRowDef="transferColumns"></tr>
|
||
<tr mat-row *matRowDef="let row; columns: transferColumns"></tr>
|
||
</table>
|
||
@if (stockTransfers.length === 0) {
|
||
<div class="empty-state">{{ 'labels.text.No records to display' | translate }}</div>
|
||
}
|
||
@if (transferTotalRecords > 0) {
|
||
<mat-paginator
|
||
[length]="transferTotalRecords"
|
||
[pageSize]="transferPageSize"
|
||
[pageSizeOptions]="pageSizeOptions"
|
||
[pageIndex]="transferPageIndex"
|
||
showFirstLastButtons
|
||
(page)="onTransferPageChange($event)"
|
||
></mat-paginator>
|
||
}
|
||
</div>
|
||
</div>
|
||
</mat-tab>
|
||
</mat-tab-group>
|
||
</div>
|