422 lines
16 KiB
HTML
422 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="dialog-wrapper">
|
||
<div class="dialog-header">
|
||
<div class="header-icon">
|
||
<mat-icon>account_balance_wallet</mat-icon>
|
||
</div>
|
||
<div class="header-text">
|
||
<h2>Apply Share Collection</h2>
|
||
<p>
|
||
Share account <strong class="mono">{{ data.accountNo }}</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 receipts…</span>
|
||
</div>
|
||
} @else {
|
||
@if (requirement) {
|
||
<div class="sc-card">
|
||
<!-- Share row -->
|
||
<div
|
||
class="sc-row"
|
||
[class.sc-ok]="shareReceipt && shareShortfall === 0"
|
||
[class.sc-bad]="shareReceipt && shareShortfall > 0"
|
||
>
|
||
<div class="sc-left">
|
||
<span class="sc-label">Share Collection</span>
|
||
<span class="sc-sub"
|
||
>Required: <b>{{ requirement.shareAmount | number: '1.2-2' }}</b></span
|
||
>
|
||
</div>
|
||
<div class="sc-right">
|
||
<span
|
||
class="sc-val"
|
||
[class.sc-val-ok]="shareReceipt && shareShortfall === 0"
|
||
[class.sc-val-bad]="shareReceipt && shareShortfall > 0"
|
||
>
|
||
{{ shareReceipt ? (shareReceipt.amount | number: '1.2-2') : '—' }}
|
||
</span>
|
||
@if (shareShortfall > 0) {
|
||
<span class="sc-tag sc-tag-bad">-{{ shareShortfall | number: '1.2-2' }}</span>
|
||
} @else if (shareReceipt && shareShortfall === 0) {
|
||
<span class="sc-tag sc-tag-ok">✓ Covered</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Entry Fee row -->
|
||
@if (hasFee) {
|
||
<div
|
||
class="sc-row sc-sep"
|
||
[class.sc-ok]="chargeReceipt && feeShortfall === 0"
|
||
[class.sc-bad]="chargeReceipt && feeShortfall > 0"
|
||
>
|
||
<div class="sc-left">
|
||
<span class="sc-label">Entry Fee</span>
|
||
<span class="sc-sub"
|
||
>Required: <b>{{ requirement.chargeAmount | number: '1.2-2' }}</b></span
|
||
>
|
||
</div>
|
||
<div class="sc-right">
|
||
<span
|
||
class="sc-val"
|
||
[class.sc-val-ok]="chargeReceipt && feeShortfall === 0"
|
||
[class.sc-val-bad]="chargeReceipt && feeShortfall > 0"
|
||
>
|
||
{{ chargeReceipt ? (chargeReceipt.amount | number: '1.2-2') : '—' }}
|
||
</span>
|
||
@if (feeShortfall > 0) {
|
||
<span class="sc-tag sc-tag-bad">-{{ feeShortfall | number: '1.2-2' }}</span>
|
||
} @else if (chargeReceipt && feeShortfall === 0) {
|
||
<span class="sc-tag sc-tag-ok">✓ Covered</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Total row -->
|
||
<div class="sc-total-row" [class.sc-total-ok]="canApply">
|
||
<span class="sc-total-lbl">Total Payable</span>
|
||
<div class="sc-total-nums">
|
||
<span class="sc-total-req mono">{{ requirement.required | number: '1.2-2' }}</span>
|
||
<span class="sc-arrow">→</span>
|
||
<span class="sc-total-got mono" [class.sc-total-got-ok]="canApply">
|
||
{{
|
||
(shareReceipt?.amount || 0) + (hasFee && chargeReceipt ? chargeReceipt.amount || 0 : 0)
|
||
| 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>
|
||
}
|
||
|
||
<!-- ── Slot 1: the share receipt ─────────────────────────────────────────── -->
|
||
<section class="slot">
|
||
<header class="slot-head">
|
||
<span class="slot-step">1</span>
|
||
<div class="slot-title">
|
||
<h3>Share Collection Receipt</h3>
|
||
<p>Posts Dr Sundry / Cr Share Capital.</p>
|
||
</div>
|
||
<button type="button" class="slot-search" (click)="openSearch('share')">
|
||
<mat-icon>search</mat-icon>
|
||
<span>Find by voucher</span>
|
||
</button>
|
||
</header>
|
||
|
||
@if (shareReceipt) {
|
||
<div class="chosen" [class.chosen-short]="shareShortfall > 0">
|
||
<div class="chosen-badge">
|
||
<mat-icon>{{ shareShortfall > 0 ? 'warning' : 'check_circle' }}</mat-icon>
|
||
</div>
|
||
<div class="chosen-main">
|
||
<span class="chosen-voucher mono">{{ shareReceipt.voucherNo }}</span>
|
||
<span class="chosen-gl">{{ glLabel(shareReceipt) }}</span>
|
||
</div>
|
||
<span class="chosen-amount mono">{{ shareReceipt.amount | number: '1.2-2' }}</span>
|
||
<button type="button" class="chosen-clear" (click)="clear('share')" aria-label="Change receipt">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
@if (shareShortfall > 0) {
|
||
<p class="slot-note note-bad">
|
||
Short by {{ shareShortfall | number: '1.2-2' }} — this receipt does not cover
|
||
{{ shareRequired | number: '1.2-2' }}.
|
||
</p>
|
||
} @else if (requirement) {
|
||
<p class="slot-note note-good">Covers the {{ shareRequired | number: '1.2-2' }} payable.</p>
|
||
}
|
||
} @else {
|
||
@if (optionsFor('share').length) {
|
||
<ul class="options">
|
||
@for (option of optionsFor('share'); track option.key) {
|
||
<li class="option" [class.option-busy]="attaching === option.key" (click)="pick('share', option)">
|
||
<span class="option-radio"></span>
|
||
<div class="option-main">
|
||
<span class="option-voucher mono">{{ option.voucherNo }}</span>
|
||
<span class="option-meta">{{ option.date }} · {{ 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>
|
||
@if (attaching === option.key) {
|
||
<mat-spinner diameter="16"></mat-spinner>
|
||
} @else if (option.collectionId !== null) {
|
||
<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 Share Collection receipt is waiting. Collect the cash at <em>Cashier › Transaction Entry</em> with
|
||
entry type <strong>Share Collection</strong>, or find an earlier voucher by number.
|
||
</p>
|
||
}
|
||
}
|
||
</section>
|
||
|
||
<!-- ── Slot 2: the entry fee receipt ─────────────────────────────────────── -->
|
||
@if (hasFee) {
|
||
<section class="slot">
|
||
<header class="slot-head">
|
||
<span class="slot-step">2</span>
|
||
<div class="slot-title">
|
||
<h3>Entry Fee Receipt</h3>
|
||
<p>Verification only — no GL entry is posted for it.</p>
|
||
</div>
|
||
<button type="button" class="slot-search" (click)="openSearch('fee')">
|
||
<mat-icon>search</mat-icon>
|
||
<span>Find by voucher</span>
|
||
</button>
|
||
</header>
|
||
|
||
@if (chargeReceipt) {
|
||
<div class="chosen" [class.chosen-short]="feeShortfall > 0">
|
||
<div class="chosen-badge">
|
||
<mat-icon>{{ feeShortfall > 0 ? 'warning' : 'check_circle' }}</mat-icon>
|
||
</div>
|
||
<div class="chosen-main">
|
||
<span class="chosen-voucher mono">{{ chargeReceipt.voucherNo }}</span>
|
||
<span class="chosen-gl">{{ glLabel(chargeReceipt) }}</span>
|
||
</div>
|
||
<span class="chosen-amount mono">{{ chargeReceipt.amount | number: '1.2-2' }}</span>
|
||
<button type="button" class="chosen-clear" (click)="clear('fee')" aria-label="Change receipt">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
@if (feeShortfall > 0) {
|
||
<p class="slot-note note-bad">
|
||
Short by {{ feeShortfall | number: '1.2-2' }} — the fee is
|
||
{{ requirement!.chargeAmount | number: '1.2-2' }}.
|
||
</p>
|
||
} @else {
|
||
<p class="slot-note note-good">Entry fee verified.</p>
|
||
}
|
||
} @else {
|
||
@if (optionsFor('fee').length) {
|
||
<ul class="options">
|
||
@for (option of optionsFor('fee'); track option.key) {
|
||
<li class="option" [class.option-busy]="attaching === option.key" (click)="pick('fee', option)">
|
||
<span class="option-radio"></span>
|
||
<div class="option-main">
|
||
<span class="option-voucher mono">{{ option.voucherNo }}</span>
|
||
<span class="option-meta">{{ option.date }} · {{ 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>
|
||
@if (attaching === option.key) {
|
||
<mat-spinner diameter="16"></mat-spinner>
|
||
} @else if (option.collectionId !== null) {
|
||
<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 separate fee receipt is waiting — find one by voucher number.</p>
|
||
}
|
||
<p class="slot-note note-muted">
|
||
Leave this empty if the fee was already collected inside the share receipt above.
|
||
</p>
|
||
}
|
||
</section>
|
||
}
|
||
|
||
@if (errorMsg) {
|
||
<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 Collection</span>
|
||
}
|
||
</button>
|
||
</mat-dialog-actions>
|
||
</div>
|
||
|
||
<!-- ── Find a voucher the list does not carry ───────────────────────────────── -->
|
||
@if (searchFor) {
|
||
<div class="overlay" (click)="closeSearch()">
|
||
<div class="popup" (click)="$event.stopPropagation()">
|
||
<header class="popup-head">
|
||
<h3>{{ searchFor === 'share' ? 'Find a share receipt' : 'Find an entry fee 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>
|
||
<select [(ngModel)]="searchGlAccountNo" [disabled]="loadingGlAccounts">
|
||
<option value="">{{ loadingGlAccounts ? 'Loading…' : 'Select the account it was paid into' }}</option>
|
||
@for (gl of glAccounts; track gl.id) {
|
||
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span>Voucher No.</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="searchVoucherNo"
|
||
placeholder="CR/18092026/0004"
|
||
(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() || !searchGlAccountNo.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.date }}</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.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 $index) {
|
||
<tr>
|
||
<td>
|
||
<span class="mono">{{ entry.glCode }}</span> {{ entry.accountName }}
|
||
</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>
|
||
}
|