feat(shares): move voucher search behind a popup, not an always-visible toolbar

The GL Account / Voucher No lookup is for the exception -- a receipt the
unattributed list and the member's own list do not already carry -- so it now
sits behind a "Search a Voucher Not Listed Below" button in both Apply
Collection and Apply Additional Shares, opening the same lookup in a popup
(matching the existing receipt-detail popup) instead of occupying screen space
whether or not anyone needs it. A successful find closes the popup and the
result appears ticked in the list behind it, same as before.
This commit is contained in:
2026-09-18 13:57:14 +05:30
parent 124f05d21d
commit dc7f668056
6 changed files with 294 additions and 101 deletions
@@ -178,61 +178,25 @@
<div class="funding-intro-banner"> <div class="funding-intro-banner">
<mat-icon class="banner-icon">info</mat-icon> <mat-icon class="banner-icon">info</mat-icon>
<span> <span>
Select the one receipt that covers this payment, or enter a voucher number to search. Click Select the one receipt that covers this payment. Click
<mat-icon class="inline-icon">visibility</mat-icon> on any row to view full details popup. <mat-icon class="inline-icon">visibility</mat-icon> on any row to view full details popup.
</span> </span>
</div> </div>
<!-- Toolbar: GL Account Select & Voucher Lookup --> <!-- A voucher not already listed below (an older receipt, or one only the member's slip
<div class="lookup-toolbar"> names) is reached through the popup, not a toolbar sitting here whether or not
<div class="gl-select-box"> anyone needs it -- the unattributed list and this member's own receipts already
<label class="toolbar-label"> <mat-icon class="mini-icon">account_tree</mat-icon> GL Account </label> cover the ordinary case. -->
<select <button
class="custom-select" mat-stroked-button
[(ngModel)]="glAccountNo" type="button"
[ngModelOptions]="{ standalone: true }" class="voucher-search-toggle-btn"
[disabled]="lookingUpVoucher || submitting || loadingGlAccounts" [disabled]="submitting"
> (click)="openVoucherSearch()"
<option value="" disabled> >
{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }} <mat-icon>search</mat-icon>
</option> <span>Search a Voucher Not Listed Below</span>
@for (gl of glAccounts; track gl.id) { </button>
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
}
</select>
</div>
<div class="voucher-lookup-box">
<label class="toolbar-label">
<mat-icon class="mini-icon">search</mat-icon> Find Specific Voucher
</label>
<div class="lookup-input-group">
<input
type="text"
class="lookup-input font-mono"
placeholder="Voucher no. (e.g. 000123)"
[(ngModel)]="voucherNo"
[disabled]="lookingUpVoucher || submitting"
(keyup.enter)="lookupVoucher()"
/>
<button
mat-flat-button
color="primary"
type="button"
class="find-btn"
[disabled]="!voucherNo.trim() || !glAccountNo.trim() || lookingUpVoucher || submitting"
(click)="lookupVoucher()"
>
@if (lookingUpVoucher) {
<mat-spinner diameter="14"></mat-spinner>
} @else {
<mat-icon>search</mat-icon>
<span>Find</span>
}
</button>
</div>
</div>
</div>
<!-- Receipts tagged "Share Collection" that belong to nobody yet. A deposit straight into <!-- Receipts tagged "Share Collection" that belong to nobody yet. A deposit straight into
a GL account names no member, so it is never attributed on its own -- and never a GL account names no member, so it is never attributed on its own -- and never
@@ -649,3 +613,85 @@
</div> </div>
</div> </div>
} }
<!-- SEARCH VOUCHER POPUP -->
@if (voucherSearchOpen) {
<div class="modal-backdrop animate-fade-in" (click)="closeVoucherSearch()">
<div class="voucher-modal-dialog voucher-search-dialog" (click)="$event.stopPropagation()">
<div class="modal-header">
<div class="header-title font-accent">
<mat-icon class="m-icon">search</mat-icon>
<span>Search a Voucher</span>
</div>
<button type="button" mat-icon-button class="close-btn" (click)="closeVoucherSearch()">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="modal-body">
<p class="voucher-search-hint">
For a receipt not already listed above — one taken on an earlier day, or a slip the member is holding.
</p>
<div class="lookup-toolbar">
<div class="gl-select-box">
<label class="toolbar-label"><mat-icon class="mini-icon">account_tree</mat-icon> GL Account</label>
<select
class="custom-select"
[(ngModel)]="glAccountNo"
[ngModelOptions]="{ standalone: true }"
[disabled]="lookingUpVoucher || submitting || loadingGlAccounts"
>
<option value="" disabled>
{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }}
</option>
@for (gl of glAccounts; track gl.id) {
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
}
</select>
</div>
<div class="voucher-lookup-box">
<label class="toolbar-label"> <mat-icon class="mini-icon">receipt</mat-icon> Voucher / Receipt No </label>
<div class="lookup-input-group">
<input
type="text"
class="lookup-input font-mono"
placeholder="Voucher no. (e.g. 000123)"
[(ngModel)]="voucherNo"
[disabled]="lookingUpVoucher || submitting"
(keyup.enter)="lookupVoucher()"
/>
<button
mat-flat-button
color="primary"
type="button"
class="find-btn"
[disabled]="!voucherNo.trim() || !glAccountNo.trim() || lookingUpVoucher || submitting"
(click)="lookupVoucher()"
>
@if (lookingUpVoucher) {
<mat-spinner diameter="14"></mat-spinner>
} @else {
<mat-icon>search</mat-icon>
<span>Find</span>
}
</button>
</div>
</div>
</div>
@if (voucherError) {
<div class="alert-box alert-error">
<mat-icon>error_outline</mat-icon>
<span>{{ voucherError }}</span>
</div>
}
</div>
<div class="modal-footer">
<button type="button" mat-stroked-button (click)="closeVoucherSearch()">Close</button>
</div>
</div>
</div>
}
@@ -335,6 +335,36 @@ mat-form-field {
} }
} }
.voucher-search-toggle-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.82rem;
font-weight: 600;
color: $accent-blue;
border-color: $primary-border;
margin-bottom: 1rem;
mat-icon {
font-size: 17px;
width: 17px;
height: 17px;
}
}
.voucher-search-hint {
font-size: 0.8rem;
color: $text-muted;
margin: 0 0 0.85rem;
}
// The shared .lookup-toolbar normally goes two-column above its own breakpoint of BROWSER width,
// which says nothing about how wide this popup itself is -- at the popup's width, two columns crowd
// the GL Account select against the voucher input. Single column reads better at this popup's width.
.voucher-search-dialog .lookup-toolbar {
grid-template-columns: 1fr;
}
/* Lookup Toolbar */ /* Lookup Toolbar */
.lookup-toolbar { .lookup-toolbar {
display: grid; display: grid;
@@ -100,6 +100,25 @@ export class ApplySharesComponent implements OnInit {
lookingUpVoucher = false; lookingUpVoucher = false;
voucherError = ''; voucherError = '';
/**
* Whether the voucher-search popup is open.
*
* The GL Account / Voucher No lookup is for the exception — a receipt the unattributed list and
* this member's own list do not carry — so it sits behind a button rather than on screen whether
* or not anyone needs it, exactly like the receipt-detail popup already does for the same reason.
*/
voucherSearchOpen = false;
openVoucherSearch(): void {
this.voucherError = '';
this.voucherSearchOpen = true;
}
closeVoucherSearch(): void {
this.voucherSearchOpen = false;
this.voucherError = '';
}
/** Voucher / Receipt No shown in the "Find Specific Voucher" box. Mirrors whichever receipt is /** Voucher / Receipt No shown in the "Find Specific Voucher" box. Mirrors whichever receipt is
* ticked below (see {@link toggleVoucher}) so a tick is visibly confirmed up there too, and * ticked below (see {@link toggleVoucher}) so a tick is visibly confirmed up there too, and
* carries whatever the counter has typed in when they search by hand instead. */ * carries whatever the counter has typed in when they search by hand instead. */
@@ -445,6 +464,9 @@ export class ApplySharesComponent implements OnInit {
// Once attached it belongs to this member, so it leaves the unattributed list rather than // Once attached it belongs to this member, so it leaves the unattributed list rather than
// sitting there inviting a second, conflicting attachment. // sitting there inviting a second, conflicting attachment.
this.unattributed = this.unattributed.filter((u) => u.voucherNo !== voucherNo); this.unattributed = this.unattributed.filter((u) => u.voucherNo !== voucherNo);
// Found it -- the popup has done its job, so it closes rather than sitting open over a
// receipt that now already shows, ticked, in the table behind it.
this.voucherSearchOpen = false;
}); });
} }
@@ -80,57 +80,21 @@
</div> </div>
} }
<!-- SHARE RECEIPT LOOKUP SECTION --> <!-- SHARE RECEIPT LOOKUP — a voucher not already sitting in the lists below (an older receipt,
or a member's own slip) is reached through the popup, not a toolbar that sat here whether or
not anyone needed it. The unattributed list and the member's own receipts already cover the
ordinary case; this is only for the exception. -->
<div class="search-section"> <div class="search-section">
<div class="section-label"> <button
<mat-icon class="sec-icon">search</mat-icon> mat-stroked-button
<span>Share Receipt Lookup</span> type="button"
</div> class="voucher-search-toggle-btn"
[disabled]="submitting"
<div class="lookup-toolbar"> (click)="openVoucherSearch()"
<div class="gl-select-box"> >
<label class="toolbar-label"><mat-icon class="mini-icon">account_tree</mat-icon> GL Account</label> <mat-icon>search</mat-icon>
<select <span>Search a Voucher Not Listed Below</span>
class="custom-select" </button>
[(ngModel)]="glAccountNo"
[disabled]="lookingUp || submitting || loadingGlAccounts"
>
<option value="" disabled>{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }}</option>
@for (gl of glAccounts; track gl.id) {
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
}
</select>
</div>
<div class="voucher-lookup-box">
<label class="toolbar-label"><mat-icon class="mini-icon">receipt</mat-icon> Voucher / Receipt No</label>
<div class="lookup-input-group">
<input
type="text"
class="lookup-input font-mono"
placeholder="Voucher no. (e.g. CR/001/2608/0001)"
[(ngModel)]="voucherNo"
[disabled]="lookingUp || submitting"
(keyup.enter)="lookupVoucher()"
/>
<button
mat-flat-button
color="primary"
type="button"
class="find-btn"
[disabled]="!voucherNo.trim() || !glAccountNo.trim() || lookingUp || submitting"
(click)="lookupVoucher()"
>
@if (lookingUp) {
<mat-spinner diameter="14"></mat-spinner>
} @else {
<mat-icon>search</mat-icon>
<span>Find</span>
}
</button>
</div>
</div>
</div>
</div> </div>
<!-- ENTRY FEE RECEIPT SECTION (If charge exists) --> <!-- ENTRY FEE RECEIPT SECTION (If charge exists) -->
@@ -575,3 +539,82 @@
</div> </div>
</div> </div>
} }
<!-- SEARCH VOUCHER POPUP -->
@if (voucherSearchOpen) {
<div class="modal-backdrop animate-fade-in" (click)="closeVoucherSearch()">
<div class="voucher-modal-dialog voucher-search-dialog" (click)="$event.stopPropagation()">
<div class="modal-header">
<div class="header-title font-accent">
<mat-icon class="m-icon">search</mat-icon>
<span>Search a Voucher</span>
</div>
<button type="button" mat-icon-button class="close-btn" (click)="closeVoucherSearch()">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="modal-body">
<p class="voucher-search-hint">
For a receipt not already listed above — one taken on an earlier day, or a slip the member is holding.
</p>
<div class="lookup-toolbar">
<div class="gl-select-box">
<label class="toolbar-label"><mat-icon class="mini-icon">account_tree</mat-icon> GL Account</label>
<select
class="custom-select"
[(ngModel)]="glAccountNo"
[disabled]="lookingUp || submitting || loadingGlAccounts"
>
<option value="" disabled>{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }}</option>
@for (gl of glAccounts; track gl.id) {
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
}
</select>
</div>
<div class="voucher-lookup-box">
<label class="toolbar-label"><mat-icon class="mini-icon">receipt</mat-icon> Voucher / Receipt No</label>
<div class="lookup-input-group">
<input
type="text"
class="lookup-input font-mono"
placeholder="Voucher no. (e.g. CR/001/2608/0001)"
[(ngModel)]="voucherNo"
[disabled]="lookingUp || submitting"
(keyup.enter)="lookupVoucher()"
/>
<button
mat-flat-button
color="primary"
type="button"
class="find-btn"
[disabled]="!voucherNo.trim() || !glAccountNo.trim() || lookingUp || submitting"
(click)="lookupVoucher()"
>
@if (lookingUp) {
<mat-spinner diameter="14"></mat-spinner>
} @else {
<mat-icon>search</mat-icon>
<span>Find</span>
}
</button>
</div>
</div>
</div>
@if (errorMsg) {
<div class="alert-box alert-error">
<mat-icon>error_outline</mat-icon>
<span>{{ errorMsg }}</span>
</div>
}
</div>
<div class="modal-footer">
<button type="button" mat-stroked-button (click)="closeVoucherSearch()">Close</button>
</div>
</div>
</div>
}
@@ -253,6 +253,36 @@ $danger-border: #fecaca;
} }
} }
.voucher-search-toggle-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.82rem;
font-weight: 600;
color: $accent-blue;
border-color: $primary-border;
mat-icon {
font-size: 17px;
width: 17px;
height: 17px;
}
}
.voucher-search-hint {
font-size: 0.8rem;
color: $text-muted;
margin: 0 0 0.85rem;
}
// The shared .lookup-toolbar normally goes two-column above 680px of BROWSER width, which says
// nothing about how wide this popup itself is -- at the popup's own ~36rem, two columns crowd the
// GL Account select against the voucher input. Single column reads better at this popup's width,
// full stop, rather than reintroducing a container query for one dialog.
.voucher-search-dialog .lookup-toolbar {
grid-template-columns: 1fr;
}
/* Lookup Toolbar - Wide Layout */ /* Lookup Toolbar - Wide Layout */
.lookup-toolbar { .lookup-toolbar {
display: grid; display: grid;
@@ -72,6 +72,25 @@ export class ApplyCollectionDialogComponent implements OnInit {
voucherNo = ''; voucherNo = '';
lookingUp = false; lookingUp = false;
/**
* Whether the voucher-search popup is open.
*
* The GL Account / Voucher No lookup is for the exception — a receipt the unattributed list and
* the member's own list do not carry — so it sits behind a button rather than on screen whether or
* not anyone needs it, exactly like the receipt-detail popup already does for the same reason.
*/
voucherSearchOpen = false;
openVoucherSearch(): void {
this.errorMsg = '';
this.voucherSearchOpen = true;
}
closeVoucherSearch(): void {
this.voucherSearchOpen = false;
this.errorMsg = '';
}
/** GL account the deposit was posted to — required, and always picked by hand. Never resolved /** GL account the deposit was posted to — required, and always picked by hand. Never resolved
* from a share product's accounting setup, so a broken or missing product mapping can never * from a share product's accounting setup, so a broken or missing product mapping can never
* make a genuine voucher unreachable. */ * make a genuine voucher unreachable. */
@@ -427,6 +446,9 @@ export class ApplyCollectionDialogComponent implements OnInit {
if (res.gl_code) { if (res.gl_code) {
this.glAccountNo = res.gl_code; this.glAccountNo = res.gl_code;
} }
// Found it -- the popup has done its job, so it closes rather than sitting open over a
// receipt that now already shows, ticked, in the table behind it.
this.voucherSearchOpen = false;
}); });
} }