diff --git a/src/app/cashier/cashier.service.ts b/src/app/cashier/cashier.service.ts index ad24b948b..83d3ba55b 100644 --- a/src/app/cashier/cashier.service.ts +++ b/src/app/cashier/cashier.service.ts @@ -401,6 +401,12 @@ export class CashierService { return this.http.post(`/shareadvancecollection/${collectionId}/apply`, data); } + /** The receipt's own real Dr/Cr GL legs — never the GL account currently picked in a search box, + * which is unrelated screen state that can name a different account than this receipt sits in. */ + getShareAdvanceGlEntries(collectionId: number): Observable { + return this.http.get(`/shareadvancecollection/${collectionId}/gl-entries`); + } + /** Every additional-share tranche awaiting approval, across every share account. */ getShareAdditionRequests(): Observable { return this.http.get('/shareadditionrequests'); @@ -413,4 +419,14 @@ export class CashierService { getShareAdditionRequest(transactionId: number): Observable { return this.http.get(`/shareadditionrequests/${transactionId}`); } + + /** + * Entry Type options for a GL credit (General / Share Collection / DD Issue / …) — the + * "GLEntryType" code under Admin > System > Manage Codes, so an admin can add another type later + * without a deployment. Empty on failure rather than erroring, since a row missing this dropdown's + * options should still be usable for every other field. + */ + getEntryTypeOptions(): Observable { + return this.http.get('/codes/name/GLEntryType/codevalues').pipe(catchError(() => of([]))); + } } diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index b6732deb6..ee14e4589 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -191,6 +191,25 @@ } + @if (showsEntryType(row)) { +
+ + +
+ } @@ -1562,6 +1581,25 @@ } + @if (showsEntryType(row)) { +
+ + +
+ }
diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index c7daa9584..a96bf4f00 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -53,6 +53,19 @@ interface TxnRow { /** DD issuance charges/commission — only used when TYPE=DD, TXN TP=Cash, AC.TP=GL */ ddCharges: number | null; sponsorBankId?: number | null; + /** + * What a GL credit is actually for — the id of a code value under "GLEntryType" (General, Share + * Collection, DD Issue, or whatever an admin has added under Admin > System > Manage Codes). + * + * A cashier Receipt (or a clerk Collection) into a GL account is the counter picking, by hand, + * any account to post cash into — deliberately, so a broken product mapping can never block a + * genuine voucher. That freedom is also the hole: two unrelated flows can land in the same GL + * account with nothing on the row itself saying which one the money was collected for, and a + * manager applying a share request can pick up a voucher that was really meant to fund a DD. This + * tag is what a day-end reconciliation groups by, and what Share Advance Collection / Sponsor DD + * issuance check before adopting a row — only shown/sent for AC.TP=GL, DR./CR.=Cr rows. + */ + entryTypeCvId?: number | null; /** Only used when this row is AC.TP=SHR inside an Enter Bank Transfer to its linked savings * account — governs the share redemption (see submitShareRedemptionTransfer()). */ resolutionNumber?: string; @@ -1864,6 +1877,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.loadCashVaultGl(); this.loadDdGstPercentage(); this.loadSponsorBanks(); + this.cashierService + .getEntryTypeOptions() + .pipe(catchError(() => of([]))) + .subscribe((options: any[]) => { + this.entryTypeOptions = options ?? []; + }); if (this.reviewMode) { this.loadRequestIntoForm(this.reviewRequestId!, 'review'); @@ -2379,6 +2398,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { return this.isDdCashGlRow(row) || this.isDdAccountDebitRow(row); } + /** + * Whether this row needs an Entry Type tag — a credit into a GL account, which is exactly the + * case where the money's real purpose isn't otherwise recorded anywhere (see TxnRow.entryTypeCvId). + * A debit out of a GL account, or any entry against a savings/current/share account, carries no + * such ambiguity — the account itself says what it's for. + */ + showsEntryType(row: TxnRow): boolean { + return row.acTyp === 'GL' && row.dbCr === 'Cr'; + } + + /** + * Entry Type choices for a GL credit — the "GLEntryType" code (Admin > System > Manage Codes), + * fetched once in ngOnInit(). A code value, not a free-text field, so the day-end "group by type" + * this exists for stays reliable; an admin extends the list there, not in this file. + */ + entryTypeOptions: { id: number; name: string }[] = []; + /** GST on DD charges only (never on the DD face value), at the admin-configured rate. */ ddGstAmountFor(row: TxnRow): number { const charges = Number(row.ddCharges) || 0; @@ -2848,7 +2884,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // Without this the backend defaults every request from this screen to DISBURSEMENT — a // Collection or Fund Transfer submitted here was being saved (and shown in the checker's // inbox) as a plain Disbursement. - requestType: isCollectionMode ? 'COLLECTION' : isFundTransfer ? 'FUND_TRANSFER' : 'DISBURSEMENT' + requestType: isCollectionMode ? 'COLLECTION' : isFundTransfer ? 'FUND_TRANSFER' : 'DISBURSEMENT', + // Same ambiguity as a cashier Receipt — a Collection credits a GL account picked by hand. + // See TxnRow.entryTypeCvId. + ...(isCollectionMode ? { entryTypeCvId: crRows[0]?.entryTypeCvId || null } : {}) }; this.saving = true; @@ -5905,6 +5944,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { chequeNo: row.chequeNo || null, bankName: row.bankName || null, denominationData: isCash && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null, + // What this GL credit is actually for — see TxnRow.entryTypeCvId. Sent for every GL deposit + // (the backend defaults to "General" when the counter didn't pick one), so Share Advance + // Collection and Sponsor DD issuance can tell whose voucher this really is before adopting it. + ...(isGl ? { entryTypeCvId: row.entryTypeCvId || null } : {}), // DD + Cash + GL breakdown — persisted so the Sponsor DD issuance screen can read the face // value/charges/GST back once this request is approved, instead of retyping them. ...(this.isDdCashGlRow(row) diff --git a/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.html b/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.html index 4497dda2c..4e7601cb0 100644 --- a/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.html +++ b/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.html @@ -6,112 +6,189 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- +
+ + +
+
+ add_card +
+
+

{{ 'Apply Additional Shares' | translate }}

+

Purchase additional share tranches via cash receipts or savings debit

+
+
+
- -
- - {{ 'labels.inputs.Request Date' | translate }} - - - - @if (applySharesForm.controls.requestedDate.hasError('required')) { - - {{ 'labels.inputs.Request Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - - {{ 'labels.inputs.Resolution Number' | translate }} - - @if (applySharesForm.controls.resolutionNumber.hasError('required')) { - - {{ 'labels.inputs.Resolution Number' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Resolution Date' | translate }} - - - - @if (applySharesForm.controls.resolutionDate.hasError('required')) { - - {{ 'labels.inputs.Resolution Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Total No. of Shares' | translate }} - - @if (applySharesForm.controls.requestedShares.hasError('required')) { - - {{ 'labels.inputs.Requested Shares' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Current Price' | translate }} - - - - - - {{ 'labels.inputs.Amount' | translate }} - - {{ 'labels.inputs.Total No. of Shares' | translate }} × - {{ 'labels.inputs.Current Price' | translate }} - - - -
-
{{ 'labels.inputs.Payment Option' | translate }}
- - Apply Collection (Cash) - Debit from Savings - + + +
+
+ assignment + Application & Resolution Details
- @if (paymentMode === 'CASH') { -
-

- The member has already paid at the counter. Tick the receipts that make up this payment, or enter a - voucher number to find one — tick more than one where they paid in instalments. The cash is sitting in - Share Sundry against them, and approving this request moves it into share capital. -

+
+ + + {{ 'labels.inputs.Request Date' | translate }} + + + + @if (applySharesForm.controls.requestedDate.hasError('required')) { + + {{ 'labels.inputs.Request Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + -
+ + + {{ 'labels.inputs.Resolution Number' | translate }} + + @if (applySharesForm.controls.resolutionNumber.hasError('required')) { + + {{ 'labels.inputs.Resolution Number' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + {{ 'labels.inputs.Resolution Date' | translate }} + + + + @if (applySharesForm.controls.resolutionDate.hasError('required')) { + + {{ 'labels.inputs.Resolution Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + +
+
+ + +
+
+ calculate + Share Purchase Calculation +
+ +
+ + + {{ 'labels.inputs.Total No. of Shares' | translate }} + + @if (applySharesForm.controls.requestedShares.hasError('required')) { + + {{ 'labels.inputs.Requested Shares' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + {{ 'labels.inputs.Current Price' | translate }} + + per share + + + +
+ {{ 'labels.inputs.Amount' | translate }} + {{ amount | number: '1.2-2' }} + {{ applySharesForm.get('requestedShares').value || 0 }} shares × + {{ sharesAccountData?.currentMarketPrice || 0 }} +
+
+
+ + +
+
+ account_balance_wallet + {{ 'labels.inputs.Payment Option' | translate }} +
+ +
+
+
+ payments +
+
+ Apply Collection (Cash) + Link receipts paid at counter sitting in Share Sundry +
+ @if (paymentMode === 'CASH') { + check_circle + } +
+ +
+
+ account_balance +
+
+ Debit from Savings + Direct debit from member's linked savings account +
+ @if (paymentMode === 'SAVINGS') { + check_circle + } +
+
+
+ + + @if (paymentMode === 'CASH') { +
+
+ + + Select the one receipt that covers this payment, or enter a voucher number to search. Click + visibility on any row to view full details popup. + +
+ + +
+
+ - -
- - @if (voucherError) { -
- error_outline - {{ voucherError }} +
+ +
+ +
- } +
+
+ @if (voucherError) { +
+ error_outline + {{ voucherError }} +
+ } + + +
@if (loadingVouchers) {
- Loading the member's receipts… + Loading member receipts…
} @else if (vouchers.length === 0) {
- info_outline -
- No unapplied receipt for this member. -
- Collect the cash first from Cashier › Transaction Entry as a deposit to the Share Sundry - GL. Once it is approved, the voucher appears here. -
+ receipt_long +
+ No unapplied receipts found for this member. +

+ Collect cash at Cashier › Transaction Entry as a deposit to Share Sundry GL. Once + posted, it will appear here. +

} @else { - - - - - - - - - - - - @for (v of vouchers; track v.id) { - - - - - - - } - - @if (selectedVoucherIds.length > 0) { - - - - - - - - } -
Voucher / ReceiptDateAmount
- - {{ v.voucher_no || v.receipt_no }}{{ v.collection_date }} - {{ v.amount | number: '1.2-2' }} -
- {{ selectedVoucherIds.length }} selected, against {{ amount | number: '1.2-2' }} - - {{ selectedVoucherTotal | number: '1.2-2' }} -
+
+ +
+
+ receipt + Vouchers / Receipts + {{ vouchers.length }} +
+
+ + +
+ + + + + + + + + + + + + + + @for (v of vouchers; track v.id) { + + + + + + + + + + } + +
#Voucher / Receipt NoDateDescriptionCollected ByAmountDetail
+ + + {{ v.voucher_no || v.receipt_no }} + {{ v.collection_date }}{{ v.note || 'Share Collection' }} + + person_outline + {{ v.collected_by_name || 'System' }} + + + {{ v.amount | number: '1.2-2' }} + + +
+
+ + + +
- @if (selectedVoucherIds.length > 0 && voucherShortfall > 0) { -
+
error_outline - The selected receipts are {{ voucherShortfall | number: '1.2-2' }} short of - {{ amount | number: '1.2-2' }}. Tick another receipt, or collect the balance at the counter first. + Selected total {{ selectedVoucherTotal | number: '1.2-2' }} is + {{ voucherShortfall | number: '1.2-2' }} short of tranche target + {{ amount | number: '1.2-2' }}. Tick another receipt or collect balance.
} }
- } +
+ } - @if (paymentMode === 'SAVINGS') { -
-

- The amount is debited from the member's linked savings into Share Sundry when this is submitted, so - their balance drops now. Approving the request then moves it into share capital. -

+ + @if (paymentMode === 'SAVINGS') { +
+
+ + + The tranche amount will be automatically debited from the member's linked savings account into Share + Sundry upon submission. + +
- @if (checkingSavings) { -
- - Checking the linked savings account… -
- } @else if (savingsEligibility) { -
- @if (savingsEligibility.savingsAccountNo) { -
- Savings account - {{ savingsEligibility.savingsAccountNo }} -
-
- Available balance - {{ savingsEligibility.savingsBalance | number: '1.2-2' }} -
- } -
- To debit - {{ amount | number: '1.2-2' }} + @if (checkingSavings) { +
+ + Checking linked savings account eligibility… +
+ } @else if (savingsEligibility) { +
+ @if (savingsEligibility.savingsAccountNo) { +
+ Savings Account No. + {{ savingsEligibility.savingsAccountNo }}
-
- - @if (!savingsEligibility.eligible) { -
- error_outline - {{ savingsEligibility.reason }} +
+ Available Balance + {{ savingsEligibility.savingsBalance | number: '1.2-2' }}
} +
+ Amount to Debit + {{ amount | number: '1.2-2' }} +
+
+ + @if (!savingsEligibility.eligible) { +
+ error_outline + {{ savingsEligibility.reason }} +
} -
- } + } +
+ } - @if (errorMsg) { -
- error_outline - {{ errorMsg }} -
- } + @if (errorMsg) { +
+ error_outline + {{ errorMsg }} +
+ } - -

- Submitting records a request for approval. The shares are issued, and the money moved out of Share Sundry, - only once it is approved — until then it appears under pending, not in the account's transactions. -

+ +
+ schedule + + Submitting records a request for approval. Shares are issued and funds moved out of Share Sundry only upon + manager approval. +
- - -
+ + +@if (viewingVoucherDetail) { + +} diff --git a/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.scss b/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.scss index 9a9fa643c..80603efdc 100644 --- a/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.scss +++ b/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.scss @@ -6,201 +6,1027 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$primary: #1a3a6b; -$border: #d8e0ec; -$muted: #6b7a90; -$danger: #b3261e; +// Modern Color Tokens +$primary: #1e3a8a; +$primary-light: #eff6ff; +$primary-border: #bfdbfe; +$accent-blue: #2563eb; +$text-main: #0f172a; +$text-muted: #64748b; +$border-color: #e2e8f0; +$border-light: #f1f5f9; +$bg-surface: #fff; +$bg-slate: #f8fafc; -.container { - max-width: 37rem; +$success: #059669; +$success-bg: #ecfdf5; +$success-border: #a7f3d0; + +$warning: #d97706; +$warning-bg: #fffbeb; +$warning-border: #fde68a; + +$danger: #dc2626; +$danger-bg: #fef2f2; +$danger-border: #fecaca; + +.action-container { + max-width: 72rem; + margin: 0.75rem auto; + padding: 0 0.5rem; } -.payment-mode { - margin: 4px 0 16px; +.action-card { + border-radius: 12px; + border: 1px solid $border-color; + background: $bg-surface; + box-shadow: 0 4px 16px -4px rgb(15 23 42 / 6%); + overflow: hidden; } -.payment-mode-label { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.4px; - color: $muted; - margin-bottom: 8px; +/* Compact Header */ +.card-header-banner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%); + color: #fff; + + .header-icon-box { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + background: rgb(255 255 255 / 20%); + border-radius: 8px; + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + color: #fff; + } + } + + .header-text { + h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 700; + color: #fff; + } + + p { + margin: 1px 0 0; + font-size: 0.78rem; + color: rgb(255 255 255 / 85%); + } + } } -.payment-mode-options { +.card-body { + padding: 0.85rem !important; +} + +/* Form Sections */ +.form-section { + background: $bg-surface; + border: 1px solid $border-light; + border-radius: 10px; + padding: 0.75rem 0.85rem; + margin-bottom: 0.65rem; + + &.financial-section { + background: $bg-slate; + border-color: $border-color; + } +} + +.section-title { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + font-weight: 700; + color: $text-main; + margin-bottom: 0.65rem; + padding-bottom: 0.35rem; + border-bottom: 1px solid $border-light; + + .sec-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $accent-blue; + } +} + +/* Compact Form Grids */ +.form-grid { + display: grid; + gap: 0.65rem; + + &.form-grid-3 { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 680px) { + grid-template-columns: 1fr; + } + } +} + +.align-items-center { + align-items: center; +} + +mat-form-field { + width: 100%; +} + +.shares-count-input { + text-align: left; +} + +// Disabled read-back field (auto-filled market price) — right-aligned so the figure sits right +// next to its "per share" suffix instead of stranded at the far left of an otherwise-empty field. +.price-input { + text-align: right; +} + +// Material fades a disabled outline field almost to invisibility by default, which next to the +// enabled "Total No. of Shares" field (a solid dark border) reads as a missing box rather than a +// deliberately read-only one. Matched back to the same border weight and label/value contrast. +.price-field ::ng-deep { + .mdc-notched-outline__leading, + .mdc-notched-outline__notch, + .mdc-notched-outline__trailing { + border-color: $border-color !important; + opacity: 1 !important; + } + + .mdc-floating-label { + color: $text-muted !important; + } + + input:disabled { + color: $text-main !important; + -webkit-text-fill-color: $text-main !important; + } +} + +.currency-label { + color: $text-muted; + font-size: 0.8rem; + white-space: nowrap; + margin-left: 0.35rem; +} + +/* Financial Stat Display Card */ +.amount-display-card { display: flex; flex-direction: column; - gap: 4px; + justify-content: center; + background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); + border: 1px solid $primary-border; + border-radius: 8px; + padding: 0.45rem 0.75rem; + height: 48px; + box-sizing: border-box; + + .amount-label { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #1e40af; + font-weight: 700; + } + + .amount-value { + font-size: 1.1rem; + font-weight: 700; + color: #1e3a8a; + font-family: 'Courier New', monospace; + line-height: 1.1; + } + + .amount-subtext { + font-size: 0.65rem; + color: #3b82f6; + } } -/* The funding route's own detail, boxed off from the share fields above it: which voucher, or - which savings account, is what the clerk checks before submitting. */ +/* Payment Option Radio Cards */ +.payment-card-group { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.65rem; + + @media (width <= 580px) { + grid-template-columns: 1fr; + } +} + +.payment-card-option { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.65rem 0.75rem; + border: 1.5px solid $border-color; + border-radius: 10px; + background: $bg-surface; + cursor: pointer; + user-select: none; + transition: all 0.15s ease; + + .option-icon-box { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + background: $bg-slate; + color: $text-muted; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + } + + .option-content { + display: flex; + flex-direction: column; + + .option-title { + font-size: 0.82rem; + font-weight: 600; + color: $text-main; + } + + .option-desc { + font-size: 0.72rem; + color: $text-muted; + } + } + + .selected-check { + margin-left: auto; + font-size: 18px; + width: 18px; + height: 18px; + color: $accent-blue; + } + + &:hover { + border-color: #93c5fd; + background: #f8fafc; + } + + &.active { + border-color: $accent-blue; + background: $primary-light; + + .option-icon-box { + background: $accent-blue; + color: #fff; + } + } +} + +/* Funding Panel */ .funding-panel { - border: 1px solid $border; + border: 1px solid $border-color; + border-radius: 10px; + padding: 0.75rem; + background: #fcfdfe; + margin-top: 0.4rem; + margin-bottom: 0.65rem; +} + +.funding-intro-banner { + display: flex; + align-items: center; + gap: 0.5rem; + background: #f0f9ff; + border: 1px solid #bae6fd; border-radius: 6px; - padding: 14px; - margin-bottom: 16px; - background: #fbfcfe; + padding: 0.45rem 0.65rem; + font-size: 0.78rem; + color: #0369a1; + margin-bottom: 0.65rem; + + .banner-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #0284c7; + flex-shrink: 0; + } + + .inline-icon { + font-size: 14px; + width: 14px; + height: 14px; + vertical-align: middle; + } } -.funding-intro { - font-size: 13px; - color: $muted; - margin: 0 0 12px; +/* Lookup Toolbar */ +.lookup-toolbar { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.65rem; + background: $bg-slate; + padding: 0.65rem; + border-radius: 8px; + border: 1px solid $border-color; + margin-bottom: 0.65rem; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } } -.gl-select-field { - margin: 0 0 10px; +.toolbar-label { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + color: $text-muted; + margin-bottom: 4px; + + .mini-icon { + font-size: 14px; + width: 14px; + height: 14px; + } } -.gl-select { +.custom-select { width: 100%; - padding: 9px 11px; - border: 1px solid $border; + height: 34px; + padding: 0 0.65rem; + border: 1px solid #cbd5e1; border-radius: 6px; - font-size: 13px; - color: $primary; + font-size: 0.8rem; + color: $text-main; background: #fff; &:focus { outline: none; - border-color: $primary; - } - - &:disabled { - background: #f4f7fb; - color: $muted; + border-color: $accent-blue; } } -.voucher-lookup { +.lookup-input-group { display: flex; - gap: 8px; - align-items: center; - margin: 0 0 12px; -} + gap: 0.4rem; -.voucher-input { - flex: 1; - min-width: 0; - padding: 9px 11px; - border: 1px solid $border; - border-radius: 6px; - font-family: 'Roboto Mono', monospace; - font-size: 13px; - letter-spacing: 0.4px; - color: $primary; + .lookup-input { + flex: 1; + height: 34px; + padding: 0 0.65rem; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 0.8rem; + color: $text-main; + background: #fff; - &:focus { - outline: none; - border-color: $primary; + &:focus { + outline: none; + border-color: $accent-blue; + } } - &:disabled { - background: #f4f7fb; - color: $muted; + .find-btn { + height: 34px; + border-radius: 6px; + padding: 0 0.75rem; + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.8rem; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } } } -.funding-loading { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 0; - font-size: 13px; - color: $muted; -} - -.funding-summary { - border: 1px solid $border; - border-radius: 6px; +/* Vouchers Array Card & Table */ +.vouchers-card { + border: 1px solid $border-color; + border-radius: 8px; background: #fff; - padding: 10px 12px; - font-size: 13px; + overflow: hidden; } -.funding-row { +.vouchers-header { display: flex; justify-content: space-between; - gap: 12px; - padding: 4px 0; - color: $primary; + align-items: center; + padding: 0.5rem 0.75rem; + background: #f1f5f9; + border-bottom: 1px solid $border-color; - > span:first-child { - color: $muted; + .vouchers-title { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.84rem; + font-weight: 700; + color: $text-main; + + .v-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: $accent-blue; + } + + .badge-count { + background: $accent-blue; + color: #fff; + font-size: 0.7rem; + font-weight: 700; + padding: 0 6px; + border-radius: 999px; + } + } + + .select-all-btn { + font-size: 0.75rem; + color: $accent-blue; + font-weight: 600; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + margin-right: 2px; + } } } -.funding-row--total { - border-top: 1px solid #eceff4; - margin-top: 4px; - padding-top: 8px; - font-weight: 500; -} - -.mono { - font-family: 'Courier New', monospace; -} - -.funding-empty { - display: flex; - gap: 10px; - align-items: flex-start; - background: #f4f7fb; - border: 1px solid $border; - border-radius: 6px; - padding: 12px; - font-size: 13px; -} - -.funding-empty-hint { - color: $muted; - margin-top: 4px; +.table-responsive { + width: 100%; + overflow-x: auto; } .receipt-table { width: 100%; - border-collapse: collapse; - font-size: 13px; - background: #fff; - border: 1px solid $border; - border-radius: 6px; + border-collapse: separate; + border-spacing: 0; + font-size: 0.78rem; th { - background: #eef2f8; - color: $primary; - font-size: 11px; + background: $bg-slate; + color: $text-muted; + font-size: 0.7rem; + font-weight: 700; text-transform: uppercase; - letter-spacing: 0.4px; - padding: 8px; + padding: 0.5rem 0.65rem; + border-bottom: 1px solid $border-color; } td { - padding: 8px; - border-bottom: 1px solid #eceff4; + padding: 0.45rem 0.65rem; + border-bottom: 1px solid $border-light; + vertical-align: middle; } - tbody tr { + .receipt-row { cursor: pointer; + transition: background 0.12s ease; &:hover { - background: #f4f7fb; + background: #f8fafc; } &.selected { - background: #e7effb; + background: #eff6ff; + + .voucher-badge { + background: #dbeafe; + color: #1e40af; + border-color: #bfdbfe; + } } } - /* The running total against the tranche amount — what decides whether the request is accepted, - and not something to be added up by eye once a second receipt is in play. */ - tfoot .total-row { - border-top: 2px solid #cbd5e1; + .col-checkbox { + width: 36px; + } + + .col-actions { + width: 44px; + } + + .custom-checkbox { + width: 16px; + height: 16px; + accent-color: $accent-blue; + cursor: pointer; + } + + .voucher-badge { + display: inline-block; + background: #f1f5f9; + color: #334155; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #cbd5e1; + font-weight: 600; + font-size: 0.78rem; + } + + .desc-cell { + color: $text-muted; + max-width: 160px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .user-chip { + display: inline-flex; + align-items: center; + gap: 3px; + background: $bg-slate; + border: 1px solid $border-color; + padding: 1px 6px; + border-radius: 999px; + font-size: 0.72rem; + color: $text-muted; + + .chip-icon { + font-size: 12px; + width: 12px; + height: 12px; + } + } + + .amount-cell { + color: $text-main; + font-size: 0.84rem; + } + + .view-detail-btn { + width: 28px; + height: 28px; + line-height: 28px; + + .eye-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $accent-blue; + } + } +} + +/* Vouchers Footer Bar */ +.vouchers-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + background: $bg-slate; + border-top: 1.5px solid #cbd5e1; + flex-wrap: wrap; + gap: 0.5rem; + + .selected-summary { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + + .summary-text { + font-size: 0.78rem; + color: $text-muted; + } + + .summary-amounts { + display: flex; + align-items: center; + gap: 0.3rem; + font-size: 0.8rem; + + .total-label { + color: $text-muted; + } + + .total-value { + color: $primary; + font-family: 'Courier New', monospace; + } + + .target-divider { + color: $text-muted; + } + + .target-value { + color: $text-muted; + } + } + } +} + +/* Coverage Status Badges */ +.coverage-status { + .status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } + + &.badge-neutral { + background: #f1f5f9; + color: #64748b; + border: 1px solid #cbd5e1; + } + + &.badge-warning { + background: $warning-bg; + color: $warning; + border: 1px solid $warning-border; + } + + &.badge-success { + background: $success-bg; + color: $success; + border: 1px solid $success-border; + } + } +} + +/* Savings Panel Details */ +.savings-card { + background: #fff; + border: 1px solid $border-color; + border-radius: 8px; + padding: 0.65rem 0.85rem; +} + +.savings-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.35rem 0; + + .lbl { + font-size: 0.78rem; + color: $text-muted; + } + + .val { + font-size: 0.82rem; + color: $text-main; + } + + &.savings-row-total { + border-top: 1px dashed $border-color; + margin-top: 0.2rem; + padding-top: 0.5rem; + + .lbl { + font-weight: 600; + color: $text-main; + } + + .val { + font-size: 0.95rem; + font-weight: 700; + } + } +} + +/* Alert Boxes */ +.alert-box { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: 8px; + font-size: 0.78rem; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + flex-shrink: 0; + } + + &.alert-error { + background: $danger-bg; + color: $danger; + border: 1px solid $danger-border; + } + + &.alert-warning { + background: $warning-bg; + color: #92400e; + border: 1px solid $warning-border; + } +} + +/* Pending Notice Banner */ +.pending-notice { + display: flex; + align-items: center; + gap: 0.5rem; + background: #f8fafc; + border: 1px solid $border-color; + border-radius: 8px; + padding: 0.5rem 0.75rem; + font-size: 0.76rem; + color: $text-muted; + + .notice-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: $accent-blue; + flex-shrink: 0; + } +} + +/* Action Buttons Bar */ +.card-actions-bar { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.65rem; + padding: 0.75rem 1rem !important; + background: $bg-slate; + border-top: 1px solid $border-color; + + .action-btn { + height: 36px; + padding: 0 1.25rem; + border-radius: 6px; + font-size: 0.82rem; + font-weight: 600; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + margin-right: 4px; + } + } +} + +/* POPUP MODAL DIALOG STYLES */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgb(15 23 42 / 50%); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.voucher-modal-dialog { + background: #fff; + border-radius: 12px; + width: 100%; + max-width: 36rem; + box-shadow: 0 20px 40px -10px rgb(15 23 42 / 25%); + border: 1px solid $border-color; + overflow: hidden; + animation: modal-scale-in 0.2s ease-out; + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.85rem 1.25rem; + background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%); + color: #fff; + + .header-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.95rem; + font-weight: 700; + + .m-icon { + font-size: 20px; + width: 20px; + height: 20px; + } + + .modal-badge { + background: rgb(255 255 255 / 20%); + padding: 2px 8px; + border-radius: 6px; + font-size: 0.8rem; + } + } + + .close-btn { + color: #fff; + width: 32px; + height: 32px; + line-height: 32px; + } + } + + .modal-body { + padding: 1rem 1.25rem; + max-height: 70vh; + overflow-y: auto; + } + + .detail-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + + @media (width <= 480px) { + grid-template-columns: 1fr; + } + } + + .detail-item { + display: flex; + flex-direction: column; + gap: 2px; + background: $bg-slate; + padding: 0.5rem 0.75rem; + border-radius: 8px; + border: 1px solid $border-light; + + .detail-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: $text-muted; + font-weight: 600; + } + + .detail-val { + font-size: 0.84rem; + color: $text-main; + } + + &.full-width { + grid-column: 1 / -1; + } + + &.highlight-amount-box { + background: $primary-light; + border-color: $primary-border; + + .detail-label { + color: #1e40af; + } + + .amount-val { + font-size: 1.25rem; + font-weight: 700; + color: #1e3a8a; + } + } + } + + .status-pill { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + width: fit-content; + + &.status-unapplied { + background: $success-bg; + color: $success; + border: 1px solid $success-border; + } + } + + .note-box { + background: #fff; + border: 1px solid $border-color; + border-radius: 6px; + padding: 0.5rem 0.65rem; + font-size: 0.8rem; + color: $text-main; + line-height: 1.4; + margin-top: 4px; + white-space: pre-wrap; + } + + .gl-entries-loading, + .gl-entries-empty { + font-size: 0.8rem; + color: $text-muted; + padding: 0.5rem 0; + } + + .gl-entries-table { + width: 100%; + border-collapse: collapse; + margin-top: 4px; + font-size: 0.8rem; + background: #fff; + border: 1px solid $border-color; + border-radius: 6px; + overflow: hidden; + + th { + background: $bg-slate; + color: $text-muted; + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 700; + padding: 0.4rem 0.6rem; + border-bottom: 1px solid $border-color; + } td { - padding-top: 10px; - color: #1f3d7a; + padding: 0.4rem 0.6rem; + border-bottom: 1px solid $border-light; + color: $text-main; } + + tr:last-child td { + border-bottom: none; + } + } + + .drcr-pill { + display: inline-block; + padding: 1px 8px; + border-radius: 999px; + font-size: 0.68rem; + font-weight: 700; + + &.drcr-pill--dr { + background: $danger-bg; + color: $danger; + border: 1px solid $danger-border; + } + + &.drcr-pill--cr { + background: $success-bg; + color: $success; + border: 1px solid $success-border; + } + } + + .modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.65rem; + padding: 0.75rem 1.25rem; + background: $bg-slate; + border-top: 1px solid $border-color; + + .modal-action-btn { + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + margin-right: 4px; + } + } + } +} + +@keyframes modal-scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); } } @@ -216,21 +1042,50 @@ $danger: #b3261e; text-align: center; } -.alert-error { - display: flex; - align-items: center; - gap: 8px; - background: #fdecea; - color: $danger; - border: 1px solid #f5c6c2; - border-radius: 6px; - padding: 10px 12px; - font-size: 13px; - margin-bottom: 12px; +.font-mono { + font-family: 'Courier New', monospace; } -.pending-hint { - font-size: 12px; - color: $muted; - margin: 0; +.font-semibold { + font-weight: 600; +} + +.font-accent { + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + sans-serif; +} + +.font-success { + color: $success !important; +} + +.mt-8 { + margin-top: 8px; +} + +.mt-10 { + margin-top: 10px; +} + +.mt-12 { + margin-top: 12px; +} + +.animate-fade-in { + animation: fade-in 0.2s ease-in-out; +} + +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } } diff --git a/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.ts b/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.ts index b071be51b..8922da149 100644 --- a/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.ts +++ b/src/app/shares/shares-account-actions/apply-shares/apply-shares.component.ts @@ -107,6 +107,15 @@ export class ApplySharesComponent implements OnInit { glAccounts: any[] = []; loadingGlAccounts = true; + /** Currently selected voucher for viewing in the detail popup. */ + viewingVoucherDetail: any = null; + + /** The receipt's own real Dr/Cr GL legs, fetched fresh per voucher — never the GL Account + * dropdown above (that is just what the by-voucher search box is currently set to, which can + * name a completely different account than the one this specific receipt actually sits in). */ + voucherGlEntries: { glCode: string; accountName: string; drCr: string; amount: number }[] = []; + loadingGlEntries = false; + /** What the member's savings can cover, for the savings route. */ savingsEligibility: { eligible: boolean; @@ -134,10 +143,43 @@ export class ApplySharesComponent implements OnInit { .pipe(catchError(() => of(null))) .subscribe((res: any) => { this.glAccounts = res?.glAccounts || []; + // Auto-picked only when there is exactly one live candidate -- the backend already narrowed + // this list to accounts that actually hold an eligible receipt today, so a single option is + // unambiguous. Two or more still means the counter chooses by hand: picking "whichever + // sorts first" among several real candidates is the same blind guess that used to land on + // Cash at Vault, just with a shorter, filtered list to guess from. + if (this.glAccounts.length === 1 && !this.glAccountNo) { + this.glAccountNo = this.glAccounts[0].gl_code; + } this.loadingGlAccounts = false; }); } + setPaymentMode(mode: string): void { + this.applySharesForm.get('paymentMode')?.setValue(mode); + } + + openVoucherDetail(voucher: any, event?: Event): void { + if (event) { + event.stopPropagation(); + } + this.viewingVoucherDetail = voucher; + this.voucherGlEntries = []; + this.loadingGlEntries = true; + this.cashierService + .getShareAdvanceGlEntries(voucher.id) + .pipe(catchError(() => of(null))) + .subscribe((res: any) => { + this.voucherGlEntries = res?.entries || []; + this.loadingGlEntries = false; + }); + } + + closeVoucherDetail(): void { + this.viewingVoucherDetail = null; + this.voucherGlEntries = []; + } + /** * Creates the apply shares form. * @@ -254,12 +296,24 @@ export class ApplySharesComponent implements OnInit { return this.selectedVoucherIds.includes(id); } + /** + * One receipt funds one tranche — no combining instalments here (unlike the first-application + * Apply Collection dialog, which still allows several). Picking a different receipt replaces the + * current one rather than adding to it, so {@link selectedVoucherIds} never holds more than one id. + */ toggleVoucher(id: number): void { - this.selectedVoucherIds = this.isVoucherSelected(id) ? this.selectedVoucherIds.filter((x) => x !== id) : [ - ...this.selectedVoucherIds, - id - ]; + const wasSelected = this.isVoucherSelected(id); + this.selectedVoucherIds = wasSelected ? [] : [id]; this.voucherError = ''; + // The GL Account picker above follows whichever receipt is ticked — its own actual GL account + // (see gl_code on the row), not left showing an unrelated account or "Select…" once a receipt + // that plainly does have one is already chosen. + if (!wasSelected) { + const voucher = this.vouchers.find((v) => v.id === id); + if (voucher?.gl_code) { + this.glAccountNo = voucher.gl_code; + } + } } /** @@ -307,19 +361,13 @@ export class ApplySharesComponent implements OnInit { this.voucherError = `Voucher ${trimmed} belongs to ${res.client_name}, not the holder of this share account.`; return; } - // Added to the selection rather than replacing it — a looked-up voucher is usually the - // second half of a part-payment, not a change of mind about the first. No amount check - // here: one instalment is not meant to cover the tranche on its own. + // Replaces the selection rather than adding to it — one receipt funds one tranche here, so + // finding a voucher by number is a different way to pick THE receipt, not a second one. this.vouchers = [ res, ...this.vouchers.filter((v) => v.id !== res.id) ]; - if (!this.isVoucherSelected(res.id)) { - this.selectedVoucherIds = [ - ...this.selectedVoucherIds, - res.id - ]; - } + this.selectedVoucherIds = [res.id]; }); } diff --git a/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.html b/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.html index 4733ce6b2..756d2d39e 100644 --- a/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.html +++ b/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.html @@ -168,6 +168,8 @@ Voucher / Receipt Date + Description + Collected By Amount @@ -186,6 +188,8 @@ {{ r.voucher_no || r.receipt_no }} {{ r.collection_date }} + {{ r.note || '—' }} + {{ r.collected_by_name || '—' }} {{ r.amount | number: '1.2-2' }} } @@ -197,7 +201,7 @@ - + {{ selectedIds.length }} {{ selectedIds.length === 1 ? 'receipt' : 'receipts' }} selected {{ selectedTotal | number: '1.2-2' }} diff --git a/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.ts b/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.ts index 7ceb51747..b556591c7 100644 --- a/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.ts +++ b/src/app/shares/shares-account-view/apply-collection-dialog/apply-collection-dialog.component.ts @@ -135,6 +135,12 @@ export class ApplyCollectionDialogComponent implements OnInit { ) .subscribe((res: any) => { this.glAccounts = res?.glAccounts || []; + // Auto-picked only when there is exactly one live candidate -- the backend already narrowed + // this list to accounts that actually hold an eligible receipt today, so a single option is + // unambiguous. Two or more still means the counter chooses by hand. + if (this.glAccounts.length === 1 && !this.glAccountNo) { + this.glAccountNo = this.glAccounts[0].gl_code; + } this.loadingGlAccounts = false; }); diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.html index d7f2c7136..4e7fc8f42 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.html @@ -68,18 +68,70 @@ Voucher / Receipt Date + Description Collected by Amount + Detail @for (v of detail.vouchers; track v.id) { - + {{ v.voucherNo || v.receiptNo }} {{ v.collectionDate }} + {{ v.note || '—' }} {{ v.collectedByName || '—' }} {{ v.amount | number: '1.2-2' }} + + + + @if (expandedVoucherId === v.id) { + + + @if (loadingGlEntriesId === v.id) { +
Loading GL entry…
+ } @else if (!glEntriesByVoucherId[v.id]?.length) { +
No GL posting recorded for this receipt.
+ } @else { + + + + + + + + + + @for (leg of glEntriesByVoucherId[v.id]; track leg.glCode + leg.drCr) { + + + + + + } + +
GL AccountDr / CrAmount
{{ leg.glCode }} — {{ leg.accountName }} + + {{ leg.drCr }} + + {{ leg.amount | number: '1.2-2' }}
+ } + + + } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.scss index 4e8a7aff8..7b9c8d19f 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.scss @@ -121,3 +121,93 @@ $danger: #b3261e; font-size: 13px; margin-top: 12px; } + +.ta-center { + text-align: center; +} + +.sard-voucher-row { + cursor: pointer; + + &:hover { + background: #f4f7fb; + } +} + +.sard-desc-cell { + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sard-detail-btn { + width: 30px; + height: 30px; + line-height: 30px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $primary; + } +} + +.sard-gl-row td { + background: #fbfcfe; + border-top: none; + padding: 4px 8px 10px; +} + +.sard-gl-loading, +.sard-gl-empty { + font-size: 12px; + color: $muted; + padding: 4px 0; +} + +.sard-gl-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + background: #fff; + border: 1px solid $border; + border-radius: 6px; + overflow: hidden; + + th { + color: $muted; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.4px; + padding: 5px 8px; + background: #f4f7fb; + border-bottom: 1px solid $border; + } + + td { + padding: 5px 8px; + border-top: 1px solid #eceff4; + } +} + +.sard-drcr-pill { + display: inline-block; + padding: 1px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + + &.sard-drcr-pill--dr { + background: #fef2f2; + color: #dc2626; + border: 1px solid #fecaca; + } + + &.sard-drcr-pill--cr { + background: #ecfdf5; + color: #059669; + border: 1px solid #a7f3d0; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.ts index 05a5267e8..2920bb3b7 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-request-detail-dialog/share-addition-request-detail-dialog.component.ts @@ -50,6 +50,12 @@ export class ShareAdditionRequestDetailDialogComponent implements OnInit { actioning = false; detail: any = null; + /** Which voucher's row is currently expanded to show its real Dr/Cr GL legs, or null if none. */ + expandedVoucherId: number | null = null; + loadingGlEntriesId: number | null = null; + /** Fetched once per voucher and kept — a checker toggling the same row twice shouldn't re-fetch. */ + glEntriesByVoucherId: { [id: number]: { glCode: string; accountName: string; drCr: string; amount: number }[] } = {}; + public dialogRef = inject(MatDialogRef); public data = inject<{ transactionId: number }>(MAT_DIALOG_DATA); @@ -76,6 +82,26 @@ export class ShareAdditionRequestDetailDialogComponent implements OnInit { return this.detail?.vouchers?.length || 0; } + /** Shows or hides a voucher's real GL posting, fetching it the first time it is opened. */ + toggleVoucherGlEntries(voucherId: number): void { + if (this.expandedVoucherId === voucherId) { + this.expandedVoucherId = null; + return; + } + this.expandedVoucherId = voucherId; + if (this.glEntriesByVoucherId[voucherId]) { + return; + } + this.loadingGlEntriesId = voucherId; + this.cashierService + .getShareAdvanceGlEntries(voucherId) + .pipe(catchError(() => of(null))) + .subscribe((res: any) => { + this.glEntriesByVoucherId[voucherId] = res?.entries || []; + this.loadingGlEntriesId = null; + }); + } + approve(): void { this.act('approveadditionalshares'); }