382 lines
14 KiB
HTML
382 lines
14 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="dialog-wrapper">
|
||
<div class="dialog-header">
|
||
<div class="header-icon">
|
||
<mat-icon>account_balance_wallet</mat-icon>
|
||
</div>
|
||
<div class="header-text">
|
||
<h2>Apply DDS Receipt</h2>
|
||
<p>
|
||
Agent <strong class="mono">{{ data.agentCode }}</strong> · {{ data.agentName }}
|
||
@if (data.glCode) {
|
||
· GL <strong class="mono">{{ data.glCode }}</strong>
|
||
}
|
||
</p>
|
||
</div>
|
||
<button type="button" mat-icon-button class="header-close" [mat-dialog-close]="false" aria-label="Close">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
|
||
<mat-dialog-content class="dialog-body">
|
||
@if (loading) {
|
||
<div class="state-block">
|
||
<mat-spinner diameter="30"></mat-spinner>
|
||
<span>Loading entries and receipts…</span>
|
||
</div>
|
||
} @else {
|
||
<!-- What the picked vouchers have to cover, and whether they do. -->
|
||
<div class="sc-card">
|
||
<div class="sc-row" [class.sc-ok]="canApply" [class.sc-bad]="pickedReceipts.size > 0 && difference !== 0">
|
||
<div class="sc-left">
|
||
<span class="sc-label">Collections up to {{ data.entryDate | date: 'dd/MM/yyyy' }}</span>
|
||
<span class="sc-sub"
|
||
>{{ pickedEntryCount }} {{ pickedEntryCount === 1 ? 'entry' : 'entries' }} to settle</span
|
||
>
|
||
</div>
|
||
<div class="sc-right">
|
||
<span class="sc-val">{{ required | number: '1.2-2' }}</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="sc-row sc-sep"
|
||
[class.sc-ok]="canApply"
|
||
[class.sc-bad]="pickedReceipts.size > 0 && difference !== 0"
|
||
>
|
||
<div class="sc-left">
|
||
<span class="sc-label">Vouchers picked</span>
|
||
<span class="sc-sub"
|
||
>{{ pickedReceipts.size }} {{ pickedReceipts.size === 1 ? 'voucher' : 'vouchers' }}</span
|
||
>
|
||
</div>
|
||
<div class="sc-right">
|
||
<span
|
||
class="sc-val"
|
||
[class.sc-val-ok]="canApply"
|
||
[class.sc-val-bad]="pickedReceipts.size > 0 && difference !== 0"
|
||
>
|
||
{{ received | number: '1.2-2' }}
|
||
</span>
|
||
@if (pickedReceipts.size > 0 && pickedDays.size > 0 && difference < 0) {
|
||
<span class="sc-tag sc-tag-bad">Short {{ -difference | number: '1.2-2' }}</span>
|
||
} @else if (pickedReceipts.size > 0 && pickedDays.size > 0 && difference > 0) {
|
||
<span class="sc-tag sc-tag-bad">Over {{ difference | number: '1.2-2' }}</span>
|
||
} @else if (canApply) {
|
||
<span class="sc-tag sc-tag-ok">✓ Matches</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sc-total-row" [class.sc-total-ok]="canApply">
|
||
<span class="sc-total-lbl">Total to Apply</span>
|
||
<div class="sc-total-nums">
|
||
<span class="sc-total-req mono">{{ required | number: '1.2-2' }}</span>
|
||
<span class="sc-arrow">→</span>
|
||
<span class="sc-total-got mono" [class.sc-total-got-ok]="canApply">{{ received | number: '1.2-2' }}</span>
|
||
<span class="sc-status-pill" [class.sc-pill-ok]="canApply" [class.sc-pill-pending]="!canApply">
|
||
{{ canApply ? '✓ Ready' : 'Pending' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 1: the days the agent wrote down ──────────────────────────────────── -->
|
||
<section class="slot">
|
||
<header class="slot-head">
|
||
<span class="slot-step">1</span>
|
||
<div class="slot-title">
|
||
<h3>Outstanding up to {{ data.entryDate | date: 'dd/MM/yyyy' }}</h3>
|
||
<p>Everything the agent wrote down and has not handed in, day by day. Open a day to see its lines.</p>
|
||
</div>
|
||
</header>
|
||
|
||
@if (days.length) {
|
||
<ul class="options">
|
||
@for (day of days; track day.date) {
|
||
<li class="option" (click)="toggleOpen(day, $event)">
|
||
<div class="option-main">
|
||
<span class="option-voucher mono">{{ day.date | date: 'dd/MM/yyyy' }}</span>
|
||
<span class="option-meta"
|
||
>{{ day.entries.length }} {{ day.entries.length === 1 ? 'entry' : 'entries' }}</span
|
||
>
|
||
</div>
|
||
<span class="option-amount mono">{{ day.total | number: '1.2-2' }}</span>
|
||
<button
|
||
type="button"
|
||
class="option-info"
|
||
(click)="toggleOpen(day, $event)"
|
||
aria-label="Show the day's lines"
|
||
>
|
||
<mat-icon>{{ openDay === day.date ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||
</button>
|
||
</li>
|
||
@if (openDay === day.date) {
|
||
<li class="day-lines">
|
||
@if (day.entries.length > 8) {
|
||
<input
|
||
class="day-search"
|
||
type="text"
|
||
[(ngModel)]="daySearch"
|
||
placeholder="Search party code or name"
|
||
autocomplete="off"
|
||
/>
|
||
}
|
||
<div class="day-scroll">
|
||
<table class="entries">
|
||
<tbody>
|
||
@for (entry of visibleEntries(day); track entry.id) {
|
||
<tr>
|
||
<td class="entry-code">{{ entry.partyCode }}</td>
|
||
<td class="entry-name">{{ entry.partyName }}</td>
|
||
<td>
|
||
{{ entry.transactionMode }}
|
||
@if (entry.advanceDays) {
|
||
· {{ entry.advanceDays }}d advance
|
||
}
|
||
@if (entry.arrearsDays) {
|
||
· {{ entry.arrearsDays }}d previous
|
||
}
|
||
</td>
|
||
<td class="num mono">{{ entry.amount | number: '1.2-2' }}</td>
|
||
</tr>
|
||
} @empty {
|
||
<tr>
|
||
<td colspan="4" class="entry-name">No line matches.</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</li>
|
||
}
|
||
}
|
||
</ul>
|
||
} @else {
|
||
<p class="slot-empty">
|
||
{{ data.agentName }} has nothing outstanding for {{ data.entryDate | date: 'dd/MM/yyyy' }}. Change the date
|
||
on the screen to the day the entries were written.
|
||
</p>
|
||
}
|
||
</section>
|
||
|
||
<!-- ── 2: the cashier's receipts ─────────────────────────────────────────── -->
|
||
<section class="slot">
|
||
<header class="slot-head">
|
||
<span class="slot-step">2</span>
|
||
<div class="slot-title">
|
||
<h3>DDS Cash Receipts</h3>
|
||
<p>Tick the vouchers. Posts Dr Agent GL / Cr DDS Customer Deposits.</p>
|
||
</div>
|
||
<button type="button" class="slot-search" (click)="openSearch()">
|
||
<mat-icon>search</mat-icon>
|
||
<span>Find by voucher</span>
|
||
</button>
|
||
</header>
|
||
|
||
@if (available.length) {
|
||
<ul class="options">
|
||
@for (option of available; track option.id) {
|
||
<li class="option" [class.option-picked]="pickedReceipts.has(option.id)" (click)="toggleReceipt(option)">
|
||
<mat-icon class="option-check">{{
|
||
pickedReceipts.has(option.id) ? 'check_box' : 'check_box_outline_blank'
|
||
}}</mat-icon>
|
||
<div class="option-main">
|
||
<span class="option-voucher mono">{{ option.voucherNo }}</span>
|
||
<span class="option-meta">{{ option.txnDate }} · {{ glLabel(option) }}</span>
|
||
@if (option.description) {
|
||
<span class="option-desc">{{ option.description }}</span>
|
||
}
|
||
</div>
|
||
<span class="option-amount mono">{{ option.amount | number: '1.2-2' }}</span>
|
||
<button type="button" class="option-info" (click)="openDetail(option, $event)" aria-label="GL entries">
|
||
<mat-icon>receipt_long</mat-icon>
|
||
</button>
|
||
</li>
|
||
}
|
||
</ul>
|
||
} @else {
|
||
<p class="slot-empty">
|
||
No DDS receipt is waiting on this agent's GL account. Collect the cash at
|
||
<em>Cashier › Transaction Entry</em> with entry type <strong>DDS</strong> and have the manager approve it,
|
||
or find an earlier voucher by number.
|
||
</p>
|
||
}
|
||
|
||
@if (pickedReceipts.size > 0 && pickedDays.size > 0 && difference !== 0) {
|
||
<p class="slot-note note-bad">
|
||
The vouchers ({{ received | number: '1.2-2' }}) have to match the day's entries ({{
|
||
required | number: '1.2-2'
|
||
}}) exactly, so every customer is credited what the agent wrote down.
|
||
</p>
|
||
}
|
||
</section>
|
||
|
||
@if (errorMsg && !searchOpen) {
|
||
<div class="alert">
|
||
<mat-icon>error_outline</mat-icon>
|
||
<span>{{ errorMsg }}</span>
|
||
</div>
|
||
}
|
||
}
|
||
</mat-dialog-content>
|
||
|
||
<mat-dialog-actions class="dialog-actions">
|
||
<button type="button" mat-stroked-button [mat-dialog-close]="false" [disabled]="submitting">Cancel</button>
|
||
<button type="button" mat-flat-button color="primary" [disabled]="!canApply" (click)="apply()">
|
||
@if (submitting) {
|
||
<mat-spinner diameter="16"></mat-spinner>
|
||
<span>Applying…</span>
|
||
} @else {
|
||
<mat-icon>check</mat-icon>
|
||
<span>Apply Receipt</span>
|
||
}
|
||
</button>
|
||
</mat-dialog-actions>
|
||
</div>
|
||
|
||
<!-- ── Find a voucher the list does not carry ───────────────────────────────── -->
|
||
@if (searchOpen) {
|
||
<div class="overlay" (click)="closeSearch()">
|
||
<div class="popup" (click)="$event.stopPropagation()">
|
||
<header class="popup-head">
|
||
<h3>Find a DDS receipt</h3>
|
||
<button type="button" mat-icon-button (click)="closeSearch()" aria-label="Close">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</header>
|
||
|
||
<div class="popup-body">
|
||
<label class="field">
|
||
<span>GL Account</span>
|
||
<div class="field-readonly mono">{{ data.glCode || '—' }} · {{ data.agentCode }}</div>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span>Voucher No.</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="searchVoucherNo"
|
||
placeholder="CR/24092026/0001"
|
||
(keyup.enter)="runSearch()"
|
||
autocomplete="off"
|
||
/>
|
||
</label>
|
||
|
||
@if (errorMsg) {
|
||
<div class="alert">
|
||
<mat-icon>error_outline</mat-icon>
|
||
<span>{{ errorMsg }}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<footer class="popup-foot">
|
||
<button type="button" mat-stroked-button (click)="closeSearch()">Cancel</button>
|
||
<button
|
||
type="button"
|
||
mat-flat-button
|
||
color="primary"
|
||
[disabled]="!searchVoucherNo.trim() || searching"
|
||
(click)="runSearch()"
|
||
>
|
||
@if (searching) {
|
||
<mat-spinner diameter="16"></mat-spinner>
|
||
} @else {
|
||
<span>Find & Use</span>
|
||
}
|
||
</button>
|
||
</footer>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ── A receipt's own GL entries ───────────────────────────────────────────── -->
|
||
@if (viewing) {
|
||
<div class="overlay" (click)="closeDetail()">
|
||
<div class="popup" (click)="$event.stopPropagation()">
|
||
<header class="popup-head">
|
||
<h3>
|
||
Voucher <span class="mono">{{ viewing.voucherNo }}</span>
|
||
</h3>
|
||
<button type="button" mat-icon-button (click)="closeDetail()" aria-label="Close">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</header>
|
||
|
||
<div class="popup-body">
|
||
<dl class="detail-list">
|
||
<div>
|
||
<dt>Date</dt>
|
||
<dd class="mono">{{ viewing.txnDate }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>Amount</dt>
|
||
<dd class="mono">{{ viewing.amount | number: '1.2-2' }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>GL Account</dt>
|
||
<dd>{{ glLabel(viewing) }}</dd>
|
||
</div>
|
||
@if (viewing.cashierName) {
|
||
<div>
|
||
<dt>Collected By</dt>
|
||
<dd>{{ viewing.cashierName }}</dd>
|
||
</div>
|
||
}
|
||
@if (viewing.managerName) {
|
||
<div>
|
||
<dt>Approved By</dt>
|
||
<dd>{{ viewing.managerName }}</dd>
|
||
</div>
|
||
}
|
||
@if (viewing.description) {
|
||
<div>
|
||
<dt>Description</dt>
|
||
<dd>{{ viewing.description }}</dd>
|
||
</div>
|
||
}
|
||
</dl>
|
||
|
||
<h4 class="detail-heading">Journal Entries</h4>
|
||
@if (loadingGlEntries) {
|
||
<div class="state-block"><mat-spinner diameter="22"></mat-spinner></div>
|
||
} @else if (viewingGlEntries?.length) {
|
||
<table class="gl-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Account</th>
|
||
<th class="num">Debit</th>
|
||
<th class="num">Credit</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (entry of viewingGlEntries; track entry.id) {
|
||
<tr>
|
||
<td>
|
||
<span class="mono">{{ entry.glCode }}</span> {{ entry.glName }}
|
||
</td>
|
||
<td class="num mono">{{ entry.drCr === 'DR' ? (entry.amount | number: '1.2-2') : '' }}</td>
|
||
<td class="num mono">{{ entry.drCr === 'CR' ? (entry.amount | number: '1.2-2') : '' }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} @else {
|
||
<p class="slot-empty">No journal entries are recorded against this receipt.</p>
|
||
}
|
||
</div>
|
||
|
||
<footer class="popup-foot">
|
||
<button type="button" mat-stroked-button (click)="closeDetail()">Close</button>
|
||
</footer>
|
||
</div>
|
||
</div>
|
||
}
|