diff --git a/src/app/cashier/clerk-share-withdrawal.service.ts b/src/app/cashier/clerk-share-withdrawal.service.ts index ff93d4294..d8eee1d70 100644 --- a/src/app/cashier/clerk-share-withdrawal.service.ts +++ b/src/app/cashier/clerk-share-withdrawal.service.ts @@ -51,6 +51,12 @@ export class ClerkShareWithdrawalService { return this.http.post(`/clerk/share-withdrawal-requests/${id}/validate`, {}); } + /** Runs the exact same checklist approve() will run, against a draft that hasn't been submitted + * yet — so the clerk sees it in the confirm dialog, before committing to the request. */ + previewValidate(data: any): Observable { + return this.http.post('/clerk/share-withdrawal-requests/preview-validate', data); + } + approve(id: number): Observable { return this.http.post(`/clerk/share-withdrawal-requests/${id}/approve`, {}); } diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index c46f71202..dcc29d252 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -11,7 +11,7 @@ import { CommonModule } from '@angular/common'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { catchError, debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, map, timeout } from 'rxjs/operators'; import { forkJoin, of, Subject } from 'rxjs'; import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; @@ -3426,6 +3426,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { meetingDate: shareRow.meetingDate, description: shareRow.description || null, settlementPaymentTypeId: savingsRow.paymentTypeId, + settlementMode: 'SAVINGS', settlementRowSnapshot }; @@ -3456,11 +3457,60 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { ? 'Pending Manager approval — redemption, settlement and account closure all post together on approval.' : 'Pending Manager approval — redemption and settlement both post together on approval.' ]; - this.showConfirmAlert('info', 'Confirm Share Withdrawal Request', confirmLines, 'Submit for Approval', () => + this.showShareWithdrawalConfirm(payload, confirmLines, () => this.doSubmitShareRedemptionTransfer(payload, shareRow, shares, linkedAcNo) ); } + /** + * Shows the confirm dialog with the same checklist the Manager will see when approving this + * request — fetched live from the backend (`previewValidate`) rather than re-implemented here, + * so it can never show the clerk something different from what the approver actually sees. A + * failing check is shown, not blocked: approve() enforces its own hard rules separately (e.g. + * Share Advance Collection applied), this list is advisory so problems surface to whoever can + * still fix them, before a manager's time is spent on a request that was always going to fail. + */ + private showShareWithdrawalConfirm( + payload: Record, + baseLines: string[], + onConfirm: () => void + ): void { + this.clerkShareWithdrawalService + .previewValidate(payload) + .pipe( + // A request that never resolves (slow network, server busy) must not leave the Submit + // button looking dead — the dialog still has to open, just without the checklist. + timeout(5000), + catchError(() => of({ __previewFailed: true })) + ) + .subscribe((res: any) => { + const checks: { label: string; pass: boolean; detail: string }[] = res?.checks ?? []; + // A failed call (most likely the backend not yet carrying the /preview-validate endpoint) + // says so explicitly rather than just omitting the section — a silently empty checklist + // looks identical to "everything passed, nothing to show", which is the one reading this + // must never give. + const checkLines = res?.__previewFailed ? [ + '', + '⚠ Validation checklist unavailable right now — the request can still be submitted.' + ] : checks.length ? [ + '', + 'Validation Checks (same as Manager approval):', + ...checks.map((c) => `${c.pass ? '✓' : '⚠'} ${c.label}: ${c.detail}`) + ] : []; + // baseLines ends with a blank line followed by the "Pending Manager approval…" footer + // sentence, which must stay last so the modal renders it as a footnote rather than a + // key/value row — so the checklist is spliced in just before that pair, not appended after. + const footerStart = + baseLines.length >= 2 && baseLines[baseLines.length - 2] === '' ? baseLines.length - 2 : baseLines.length; + const lines = [ + ...baseLines.slice(0, footerStart), + ...checkLines, + ...baseLines.slice(footerStart) + ]; + this.showConfirmAlert('info', 'Confirm Share Withdrawal Request', lines, 'Submit for Approval', onConfirm); + }); + } + /** * Share redemption settled to a GL account the clerk picks on the CR row, instead of the * member's linked savings account — mirrors submitShareRedemptionTransfer() above, but the @@ -3572,7 +3622,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { ? 'Pending Manager approval — redemption, settlement and account closure all post together on approval.' : 'Pending Manager approval — redemption and settlement both post together on approval.' ]; - this.showConfirmAlert('info', 'Confirm Share Withdrawal Request', confirmLines, 'Submit for Approval', () => + this.showShareWithdrawalConfirm(payload, confirmLines, () => this.doSubmitShareGlSettlementTransfer(payload, shareRow, shares, glRow.acNo) ); } diff --git a/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html b/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html index b47ead9e9..4de97690b 100644 --- a/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html +++ b/src/app/clerk/share-withdrawal-request/share-withdrawal-request.component.html @@ -129,6 +129,31 @@ * Date of Meeting + + @if (!isReview && settlementMode === 'GL') { + + }