375 lines
13 KiB
HTML
375 lines
13 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>trending_up</mat-icon>
|
||
</div>
|
||
<div class="header-text">
|
||
<h2>Apply Additional Shares</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>
|
||
|
||
<!-- ── Shares / Price / Amount strip — outside scroll area so always visible ── -->
|
||
<div class="shares-subheader">
|
||
<div class="shares-strip">
|
||
<div class="shares-cell shares-cell-input">
|
||
<span class="shares-label">Shares</span>
|
||
<input
|
||
class="req-input mono"
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
inputmode="numeric"
|
||
placeholder="0"
|
||
[(ngModel)]="requestedShares"
|
||
(ngModelChange)="onSharesChange()"
|
||
/>
|
||
</div>
|
||
<div class="shares-cell">
|
||
<span class="shares-label">Unit Price</span>
|
||
<span class="shares-value mono">{{ unitPrice | number: '1.2-2' }}</span>
|
||
</div>
|
||
<div class="shares-cell shares-cell-total">
|
||
<span class="shares-label">Total Amount</span>
|
||
<span class="shares-value mono">{{ amount | number: '1.2-2' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<mat-dialog-content class="dialog-body">
|
||
<!-- ── Step 1: Board Resolution ──────────────────────────────────────── -->
|
||
<section class="slot">
|
||
<header class="slot-head">
|
||
<span class="slot-step">1</span>
|
||
<div class="slot-title">
|
||
<h3>Board Resolution</h3>
|
||
<p>The meeting that authorised this tranche, not the one the account was opened under.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="field-row">
|
||
<label class="field">
|
||
<span>Resolution No.</span>
|
||
<input type="text" [(ngModel)]="resolutionNumber" placeholder="e.g. BR/2026/14" autocomplete="off" />
|
||
</label>
|
||
<label class="field">
|
||
<span>Meeting Date</span>
|
||
<input type="date" [(ngModel)]="resolutionDate" />
|
||
</label>
|
||
<label class="field">
|
||
<span>Request Date</span>
|
||
<input type="date" [(ngModel)]="requestedDate" />
|
||
</label>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── Step 2: Funding ───────────────────────────────────────────────── -->
|
||
<section class="slot">
|
||
<header class="slot-head">
|
||
<span class="slot-step">2</span>
|
||
<div class="slot-title">
|
||
<h3>Funding</h3>
|
||
<p>Approval posts Dr Sundry / Cr Share Capital either way.</p>
|
||
</div>
|
||
@if (funding === 'CASH') {
|
||
<button type="button" class="slot-search" (click)="openSearch()">
|
||
<mat-icon>search</mat-icon>
|
||
<span>Find by voucher</span>
|
||
</button>
|
||
}
|
||
</header>
|
||
|
||
<div class="funding-choice">
|
||
<button
|
||
type="button"
|
||
class="funding-btn"
|
||
[class.funding-on]="funding === 'CASH'"
|
||
(click)="chooseFunding('CASH')"
|
||
>
|
||
<mat-icon>payments</mat-icon>
|
||
<span class="funding-name">Cash</span>
|
||
<span class="funding-hint">Already paid at the counter</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="funding-btn"
|
||
[class.funding-on]="funding === 'SAVINGS'"
|
||
(click)="chooseFunding('SAVINGS')"
|
||
>
|
||
<mat-icon>savings</mat-icon>
|
||
<span class="funding-name">Savings</span>
|
||
<span class="funding-hint">Debited on submit</span>
|
||
</button>
|
||
</div>
|
||
|
||
@if (funding === 'CASH') {
|
||
@if (loadingReceipts) {
|
||
<div class="state-block">
|
||
<mat-spinner diameter="24"></mat-spinner>
|
||
<span>Loading receipts…</span>
|
||
</div>
|
||
} @else if (receipt) {
|
||
<div class="chosen" [class.chosen-short]="shortfall > 0">
|
||
<div class="chosen-badge">
|
||
<mat-icon>{{ shortfall > 0 ? 'warning' : 'check_circle' }}</mat-icon>
|
||
</div>
|
||
<div class="chosen-main">
|
||
<span class="chosen-voucher mono">{{ receipt.voucherNo }}</span>
|
||
<span class="chosen-gl">{{ glLabel(receipt) }}</span>
|
||
</div>
|
||
<span class="chosen-amount mono">{{ receipt.amount | number: '1.2-2' }}</span>
|
||
<button type="button" class="chosen-clear" (click)="clearReceipt()" aria-label="Change receipt">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
@if (shortfall > 0) {
|
||
<p class="slot-note note-bad">
|
||
Short by {{ shortfall | number: '1.2-2' }} — this receipt does not cover {{ amount | number: '1.2-2' }}.
|
||
</p>
|
||
} @else {
|
||
<p class="slot-note note-good">Covers the {{ amount | number: '1.2-2' }} tranche.</p>
|
||
}
|
||
} @else if (available.length) {
|
||
<ul class="options">
|
||
@for (option of available; track option.key) {
|
||
<li class="option" [class.option-busy]="attaching === option.key" (click)="pick(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>
|
||
}
|
||
}
|
||
|
||
@if (funding === 'SAVINGS') {
|
||
@if (checkingSavings) {
|
||
<div class="state-block">
|
||
<mat-spinner diameter="24"></mat-spinner>
|
||
<span>Checking the linked savings account…</span>
|
||
</div>
|
||
} @else if (savings) {
|
||
<div class="chosen" [class.chosen-short]="!savings.eligible">
|
||
<div class="chosen-badge">
|
||
<mat-icon>{{ savings.eligible ? 'check_circle' : 'warning' }}</mat-icon>
|
||
</div>
|
||
<div class="chosen-main">
|
||
<span class="chosen-voucher">
|
||
@if (savings.savingsAccountNo) {
|
||
Savings <span class="mono">{{ savings.savingsAccountNo }}</span>
|
||
} @else {
|
||
Linked savings account
|
||
}
|
||
</span>
|
||
<span class="chosen-gl">
|
||
{{ savings.eligible ? 'Debited into Share Sundry on submit.' : savings.reason }}
|
||
</span>
|
||
</div>
|
||
@if (savings.savingsBalance !== null) {
|
||
<span class="chosen-amount mono">{{ savings.savingsBalance | number: '1.2-2' }}</span>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<p class="slot-note note-muted">Enter the number of shares to check the member's balance.</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]="!canSubmit" (click)="submit()">
|
||
@if (submitting) {
|
||
<mat-spinner diameter="16"></mat-spinner>
|
||
<span>Submitting…</span>
|
||
} @else {
|
||
<ng-container>
|
||
<mat-icon>check</mat-icon>
|
||
<span>Submit Request</span>
|
||
</ng-container>
|
||
}
|
||
</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 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>
|
||
}
|