A cash deposit posted to the wrong account is moved to the right one in four steps: unpass the voucher, a manager approves, the cashier enters the correct account, a manager approves again. Nothing changes on any account until the final approval. - Voucher Correction page with four-step strip and popups: today's vouchers to unpass (admin or clerk), the unpassed vouchers waiting for their correct account (cashier) with the wrong and correct account side by side and the balance check, and the caller's own requests. A cashier whose customer has used the money closes the request and deposits separately. - Reached from the cashier bar, the clerk bar and the Cashier menu; the manager reviews both steps under Approvals > Voucher Corrections, laid out like the other approval tabs. Routes for Cash Transfers approvals are added to the same tab bar. - A Correction column on My Vouchers, the day book and the voucher register (and in the voucher detail popup) says whether a voucher is unpassed, revoked in favour of another, a correction, or closed, with the reason. - The deposit taken off the wrong account no longer shows on its passbook or transaction list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
230 lines
9.1 KiB
HTML
230 lines
9.1 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="pg">
|
|
<header class="pg-head">
|
|
<div>
|
|
<div class="pg-eyebrow">MY WORK</div>
|
|
<h1 class="pg-title">My Vouchers</h1>
|
|
<p class="pg-sub">
|
|
Everything you entered and everything you approved — receipts, payments and journals in one list.
|
|
</p>
|
|
</div>
|
|
<div class="pg-toolbar">
|
|
<label class="pg-field">
|
|
<span>Period</span>
|
|
<select [(ngModel)]="period" (change)="load()">
|
|
<option value="DAY">Day</option>
|
|
<option value="MONTH">Month</option>
|
|
</select>
|
|
</label>
|
|
<label class="pg-field">
|
|
<span>{{ period === 'DAY' ? 'Date' : 'Month of' }}</span>
|
|
<input type="date" [(ngModel)]="anchor" (change)="load()" />
|
|
</label>
|
|
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
|
|
<mat-icon>refresh</mat-icon> Run
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
@if (error) {
|
|
<div class="pg-alert pg-alert--error">{{ error }}</div>
|
|
}
|
|
|
|
@if (loading) {
|
|
<div class="pg-empty">Loading…</div>
|
|
}
|
|
|
|
@if (report && !loading) {
|
|
<div class="mw-who">
|
|
<strong>{{ report.displayName || report.username }}</strong>
|
|
<span class="mw-who__sep">·</span>
|
|
{{ report.from | date: 'dd MMM yyyy' }}
|
|
@if (report.from !== report.to) {
|
|
<span> to {{ report.to | date: 'dd MMM yyyy' }}</span>
|
|
}
|
|
</div>
|
|
|
|
<!-- The day at a glance, read off the same rows the table below shows: narrow "My part" to
|
|
Entered or Approved and these five follow, instead of staying pinned to the combined
|
|
day total regardless of the filter. -->
|
|
<div class="mw-tiles">
|
|
<div class="mw-tile">
|
|
<div class="mw-tile__label">Received</div>
|
|
<div class="mw-tile__value mw-tile__value--cr">₹{{ receivedTotal | number: '1.2-2' }}</div>
|
|
</div>
|
|
<div class="mw-tile">
|
|
<div class="mw-tile__label">Payments</div>
|
|
<div class="mw-tile__value mw-tile__value--dr">₹{{ paymentTotal | number: '1.2-2' }}</div>
|
|
</div>
|
|
<div class="mw-tile">
|
|
<div class="mw-tile__label">Journal</div>
|
|
<div class="mw-tile__value">₹{{ journalTotal | number: '1.2-2' }}</div>
|
|
</div>
|
|
<div class="mw-tile">
|
|
<div class="mw-tile__label">Entered</div>
|
|
<div class="mw-tile__value">{{ raisedCount }}</div>
|
|
</div>
|
|
<div class="mw-tile">
|
|
<div class="mw-tile__label">Approved</div>
|
|
<div class="mw-tile__value">{{ verifiedCount }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (report.count > 0) {
|
|
<div class="mw-filters">
|
|
<label class="pg-field">
|
|
<span>Side</span>
|
|
<select [(ngModel)]="sideFilter">
|
|
<option value="">All</option>
|
|
<option value="CR">Received</option>
|
|
<option value="DR">Payment</option>
|
|
<option value="JV">Journal</option>
|
|
</select>
|
|
</label>
|
|
<label class="pg-field">
|
|
<span>My part</span>
|
|
<select [(ngModel)]="roleFilter">
|
|
<option value="">All</option>
|
|
<option value="RAISED">Entered</option>
|
|
<option value="VERIFIED">Approved</option>
|
|
<option value="RAISED_AND_VERIFIED">Entered & approved</option>
|
|
</select>
|
|
</label>
|
|
@if (isFiltered) {
|
|
<button type="button" class="pg-btn" (click)="clearFilters()"><mat-icon>clear</mat-icon> Clear</button>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (rows.length === 0) {
|
|
<div class="pg-empty">
|
|
@if (report.count === 0) {
|
|
Nothing recorded against you for this period.
|
|
} @else {
|
|
No vouchers match these filters — {{ report.count }} hidden.
|
|
}
|
|
</div>
|
|
} @else {
|
|
<div class="mw-count">
|
|
Showing {{ rows.length }}
|
|
@if (isFiltered) {
|
|
<span>of {{ report.count }}</span>
|
|
}
|
|
{{ rows.length === 1 ? 'voucher' : 'vouchers' }}
|
|
<span class="mw-count__total">Total ₹{{ filteredTotal | number: '1.2-2' }}</span>
|
|
</div>
|
|
|
|
<!-- One table per part played, not one long list. Entering a request and passing somebody
|
|
else's are different jobs answerable to different people, and each carries its own total
|
|
so a maker's tally and a checker's can both be read off the page. -->
|
|
@for (g of groups; track g.key) {
|
|
<div class="pg-card mw-group" [class]="'mw-group--' + g.key">
|
|
<div class="mw-group__head">
|
|
<strong>{{ g.label }}</strong>
|
|
<span class="mw-group__hint">{{ g.hint }}</span>
|
|
<span class="mw-group__count">
|
|
{{ g.rows.length }} {{ g.rows.length === 1 ? 'voucher' : 'vouchers' }}
|
|
<span class="mw-group__total">₹{{ g.total | number: '1.2-2' }}</span>
|
|
</span>
|
|
</div>
|
|
<div class="pg-table-wrap">
|
|
<table class="pg-table mw-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Voucher</th>
|
|
<th>Side</th>
|
|
<th>Type</th>
|
|
<th>Debit</th>
|
|
<th>Credit</th>
|
|
<th class="num">Amount</th>
|
|
<th>My part</th>
|
|
<th>Status</th>
|
|
<th>Correction</th>
|
|
<th>Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (r of g.rows; track r.voucherNo) {
|
|
<!-- The left edge repeats the section's own colour, so a row still says which part it
|
|
is when the heading has scrolled away. Clicking anywhere on the row opens the full
|
|
slip -- every line, not just the first debit and first credit this table has room
|
|
to show. -->
|
|
<tr
|
|
[class]="'mw-row mw-row--' + r.myRole"
|
|
class="mw-row--clickable"
|
|
(click)="openDetail(r.voucherNo)"
|
|
[attr.aria-busy]="loadingDetailFor === r.voucherNo"
|
|
>
|
|
<td class="mw-vno">
|
|
{{ r.voucherNo }}
|
|
@if (loadingDetailFor === r.voucherNo) {
|
|
<mat-icon class="mw-vno__spinner" inline>hourglass_empty</mat-icon>
|
|
}
|
|
</td>
|
|
<td>
|
|
<span class="mw-badge" [class]="'mw-badge--' + (r.side || 'JV')">{{ sideLabel(r.side) }}</span>
|
|
</td>
|
|
<td class="mw-type">{{ r.voucherType }}</td>
|
|
<td>
|
|
{{ r.debitParty || '—' }}
|
|
@if (r.debitAccount) {
|
|
<span class="mw-acct">{{ r.debitAccount }}</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
{{ r.creditParty || '—' }}
|
|
@if (r.creditAccount) {
|
|
<span class="mw-acct">{{ r.creditAccount }}</span>
|
|
}
|
|
@if (r.extraLines > 0) {
|
|
<span class="mw-more" title="Click the row to see every line">+{{ r.extraLines }} more</span>
|
|
}
|
|
</td>
|
|
<td class="num">₹{{ r.amount | number: '1.2-2' }}</td>
|
|
<td>
|
|
<span class="mw-part" [class]="'mw-part--' + r.myRole">
|
|
<mat-icon class="mw-part__icon">{{ roleIcon(r.myRole) }}</mat-icon>
|
|
{{ roleLabel(r.myRole) }}
|
|
</span>
|
|
<!-- Whose entry it was, when it was not this user's — that is the whole point of
|
|
an approval row appearing here at all. -->
|
|
@if (r.myRole === 'VERIFIED' && r.raisedBy) {
|
|
<span class="mw-acct">by {{ r.raisedBy }}</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<span class="mw-status" [class]="'mw-status--' + statusClass(r.verificationStatus)">
|
|
{{ statusLabel(r.verificationStatus) }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<!-- Where the voucher stands after a correction: unpassed, revoked, closed or corrected. -->
|
|
<mifosx-voucher-status
|
|
[status]="r.voucherStatus"
|
|
[revokedBy]="r.revokedByVoucherNo"
|
|
[replaces]="r.replacesVoucherNo"
|
|
[note]="r.correctionNote"
|
|
></mifosx-voucher-status>
|
|
@if (!r.voucherStatus || r.voucherStatus === 'ACTIVE') {
|
|
<span class="mw-acct">—</span>
|
|
}
|
|
</td>
|
|
<td class="mw-time">{{ r.createdAt | date: 'HH:mm' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
</div>
|