Files
cbsnexora/src/app/chequebook/stock-management/stock-management.component.html
T
niteshandClaude Sonnet 5 97d0545f07
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
fix(chequebook,ctsclearing): surface global stock allocation in UI; fix decided-state check
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>
2026-07-23 17:08:10 +05:30

340 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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 }} &times; {{ 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>&nbsp;{{ '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
}}
&times; {{ 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>&nbsp;{{ '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>