271 lines
11 KiB
HTML
271 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="pg">
|
||
<header class="pg-head">
|
||
<div>
|
||
<h1 class="pg-title">{{ title }}</h1>
|
||
<p class="pg-sub">
|
||
Every voucher raised on the selected date — cash deposits, withdrawals, disbursements, collections, interest and
|
||
journals, with each person's own credit, debit and net.
|
||
</p>
|
||
</div>
|
||
<div class="pg-toolbar">
|
||
<button type="button" class="pg-btn" (click)="shiftDate(-1)" title="Previous day">
|
||
<mat-icon>chevron_left</mat-icon>
|
||
</button>
|
||
<label class="pg-field">
|
||
<span>Date</span>
|
||
<input type="date" [value]="date" (change)="date = $any($event.target).value; load()" />
|
||
</label>
|
||
<button type="button" class="pg-btn" (click)="shiftDate(1)" title="Next day">
|
||
<mat-icon>chevron_right</mat-icon>
|
||
</button>
|
||
<button type="button" class="pg-btn" (click)="goToday()">Today</button>
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
|
||
<mat-icon>refresh</mat-icon> Reload
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Filters -->
|
||
<div class="db-filters">
|
||
<label class="pg-field">
|
||
<span>Side</span>
|
||
<select [(ngModel)]="side" (change)="load()">
|
||
<option value="">All</option>
|
||
<option value="CR">CR · Credit (money in)</option>
|
||
<option value="DR">DR · Debit (money out)</option>
|
||
</select>
|
||
</label>
|
||
<label class="pg-field">
|
||
<span>Status</span>
|
||
<select [(ngModel)]="status" (change)="load()">
|
||
<option value="">All</option>
|
||
<option value="APPROVED">Approved</option>
|
||
<option value="PENDING">Pending</option>
|
||
<option value="REJECTED">Rejected</option>
|
||
</select>
|
||
</label>
|
||
<label class="pg-field db-filters__search">
|
||
<span>Search</span>
|
||
<input type="text" [(ngModel)]="search" placeholder="Voucher no, account, party, payment type, narration…" />
|
||
</label>
|
||
<button type="button" class="pg-btn" (click)="exportCsv()" [disabled]="filteredRows.length === 0">
|
||
<mat-icon>download</mat-icon> CSV
|
||
</button>
|
||
</div>
|
||
|
||
@if (errorMsg) {
|
||
<div class="pg-alert pg-alert--error">{{ errorMsg }}</div>
|
||
}
|
||
@if (loading) {
|
||
<div class="pg-empty">Loading…</div>
|
||
}
|
||
|
||
@if (!loading && loaded && rows.length === 0) {
|
||
<div class="pg-empty">
|
||
<mat-icon>receipt_long</mat-icon>
|
||
<p>No vouchers were raised on {{ date }}.</p>
|
||
</div>
|
||
}
|
||
|
||
@if (!loading && filteredRows.length > 0) {
|
||
<!-- Totals bar. Reflects what is on screen, so a search or side filter still reconciles. -->
|
||
<div class="closing-summary">
|
||
<div class="closing-summary__item closing-summary__item--in">
|
||
<span class="closing-summary__label">Credit · CR (money in)</span>
|
||
<span class="closing-summary__value">₹{{ shownCredit | number: '1.2-2' }}</span>
|
||
</div>
|
||
<div class="closing-summary__sep">−</div>
|
||
<div class="closing-summary__item closing-summary__item--out">
|
||
<span class="closing-summary__label">Debit · DR (money out)</span>
|
||
<span class="closing-summary__value">₹{{ shownDebit | number: '1.2-2' }}</span>
|
||
</div>
|
||
<div class="closing-summary__sep">=</div>
|
||
<div
|
||
class="closing-summary__item"
|
||
[class.closing-summary__item--net-pos]="shownCredit - shownDebit >= 0"
|
||
[class.closing-summary__item--net-neg]="shownCredit - shownDebit < 0"
|
||
>
|
||
<span class="closing-summary__label">Net</span>
|
||
<span class="closing-summary__value">₹{{ shownCredit - shownDebit | number: '1.2-2' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="db-note">
|
||
Totals count approved vouchers only — pending and rejected rows are listed but never move the money.
|
||
</p>
|
||
|
||
<!-- The day per person, for the officer who signs the book off. The sections below trace one
|
||
voucher at a time; this is the other reading a branch needs — what each member of staff
|
||
handled, and whether their column adds up. Built from whoever appears in the day's rows, so
|
||
one person working alone gets one line and five people get five. -->
|
||
@if (byUser.length > 0) {
|
||
<div class="pg-card db-section db-byuser">
|
||
<div class="db-section__head">
|
||
<strong>Officer Verification Sheet</strong>
|
||
<span class="db-muted">Each person's own credit, debit and net for the day</span>
|
||
<span class="db-section__count"> {{ byUser.length }} {{ byUser.length === 1 ? 'person' : 'people' }} </span>
|
||
</div>
|
||
<div class="pg-table-wrap">
|
||
<table class="pg-table db-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Handled by</th>
|
||
<th class="num">Items</th>
|
||
<th class="num">Credit (CR)</th>
|
||
<th class="num">Debit (DR)</th>
|
||
<th class="num">Net</th>
|
||
<th class="num">Pending</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (u of byUser; track u.name) {
|
||
<tr>
|
||
<td class="db-byuser__name">{{ u.name }}</td>
|
||
<td class="num">{{ u.items }}</td>
|
||
<td class="num db-byuser__cr">₹{{ u.credit | number: '1.2-2' }}</td>
|
||
<td class="num db-byuser__dr">₹{{ u.debit | number: '1.2-2' }}</td>
|
||
<td
|
||
class="num db-byuser__net"
|
||
[class.db-byuser__net--pos]="u.net >= 0"
|
||
[class.db-byuser__net--neg]="u.net < 0"
|
||
>
|
||
₹{{ u.net | number: '1.2-2' }}
|
||
</td>
|
||
<td class="num">
|
||
@if (u.pending > 0) {
|
||
<span class="db-badge db-badge--pending">{{ u.pending }}</span>
|
||
} @else {
|
||
—
|
||
}
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
<!-- The sheet's own total, which must equal the day's totals above. That equality is
|
||
what makes the sheet verifiable rather than just informative. -->
|
||
<tfoot>
|
||
<tr class="db-byuser__total">
|
||
<td>All staff</td>
|
||
<td class="num">{{ byUserTotals.items }}</td>
|
||
<td class="num db-byuser__cr">₹{{ byUserTotals.credit | number: '1.2-2' }}</td>
|
||
<td class="num db-byuser__dr">₹{{ byUserTotals.debit | number: '1.2-2' }}</td>
|
||
<td
|
||
class="num db-byuser__net"
|
||
[class.db-byuser__net--pos]="byUserTotals.net >= 0"
|
||
[class.db-byuser__net--neg]="byUserTotals.net < 0"
|
||
>
|
||
₹{{ byUserTotals.net | number: '1.2-2' }}
|
||
</td>
|
||
<td class="num">
|
||
@if (byUserTotals.pending > 0) {
|
||
{{ byUserTotals.pending }}
|
||
} @else {
|
||
—
|
||
}
|
||
</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- One table per source register rather than one long list. The two read differently: the
|
||
cashier's rows are cash that actually crossed the counter, the clerk's are requests nobody
|
||
has actioned yet. Mixed together they invited the wrong sum. -->
|
||
@for (s of sections; track s.key) {
|
||
<!-- The section's own colour on the left edge: cash that actually crossed the counter, work
|
||
still waiting to be actioned, and postings the system made are three different things to
|
||
a person closing the day. -->
|
||
<div [class]="'pg-card db-section db-section--' + s.key">
|
||
<div class="db-section__head">
|
||
<strong>{{ s.label }}</strong>
|
||
<span class="db-muted">{{ s.hint }}</span>
|
||
<span class="db-section__count">{{ s.rows.length }} row{{ s.rows.length === 1 ? '' : 's' }}</span>
|
||
</div>
|
||
<table class="pg-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Voucher No</th>
|
||
<th>Dr/Cr</th>
|
||
<th>Type</th>
|
||
<th>Account</th>
|
||
<th>Party</th>
|
||
<th class="num">Amount</th>
|
||
<th>Payment</th>
|
||
<th>Status</th>
|
||
<th>By</th>
|
||
<th>Approved By</th>
|
||
<th>Time</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (r of s.rows; track r.voucherNo ?? r.section + r.txnId) {
|
||
<tr [class.db-row--cr]="r.side === 'CR'" [class.db-row--dr]="r.side === 'DR'">
|
||
<td>
|
||
<span class="pg-chip">{{ r.voucherNo ?? '—' }}</span>
|
||
</td>
|
||
<td>
|
||
<span class="db-side" [class.db-side--cr]="r.side === 'CR'" [class.db-side--dr]="r.side === 'DR'">
|
||
{{ r.side }}
|
||
</span>
|
||
</td>
|
||
<td>{{ r.kind }}</td>
|
||
<td>
|
||
{{ r.accountNo ?? '—' }}
|
||
@if (r.accountType) {
|
||
<span class="db-muted">· {{ r.accountType }}</span>
|
||
}
|
||
</td>
|
||
<td>{{ r.party ?? '—' }}</td>
|
||
<td class="num">₹{{ r.amount | number: '1.2-2' }}</td>
|
||
<td>{{ r.paymentTypeName ?? '—' }}</td>
|
||
<td>
|
||
<span [class]="statusClass(r.status)">{{ r.status }}</span>
|
||
</td>
|
||
<td>{{ r.handledBy ?? '—' }}</td>
|
||
<td>
|
||
{{ r.approvedBy ?? '—' }}
|
||
@if (r.approvedBy && r.actionedAt) {
|
||
<span class="db-muted">· {{ timeOf(r.actionedAt) }}</span>
|
||
}
|
||
</td>
|
||
<td>{{ timeOf(r.submittedAt) }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td colspan="5">{{ s.label }} subtotal</td>
|
||
<td class="num">
|
||
@if (s.credit > 0 && s.debit > 0) {
|
||
₹{{ s.credit | number: '1.2-2' }} CR · ₹{{ s.debit | number: '1.2-2' }} DR
|
||
} @else if (s.credit > 0) {
|
||
₹{{ s.credit | number: '1.2-2' }} CR
|
||
} @else if (s.debit > 0) {
|
||
₹{{ s.debit | number: '1.2-2' }} DR
|
||
} @else {
|
||
—
|
||
}
|
||
</td>
|
||
<td colspan="5"></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
@if (!loading && loaded && rows.length > 0 && filteredRows.length === 0) {
|
||
<div class="pg-empty">No voucher matches “{{ search }}”.</div>
|
||
}
|
||
</div>
|