The Clerk Dashboard's standalone Share Withdrawal Request form never offered a GL settlement destination -- it always submitted SAVINGS, silently, while the Cashier's Transaction Entry screen already let a clerk pick any postable GL account on its CR row. Added the same "Settlement Destination" choice (Savings / GL Account) with a searchable GL selector here too, so both entry points to the same backend endpoint behave the same way. Both share-withdrawal confirm dialogs (savings settlement and GL settlement, in transaction-entry.component.ts) now call the backend's new preview-validate endpoint before showing the confirm dialog, so a clerk sees the same checklist a manager will see on approval -- a missing GL mapping, an unverified KYC, an open AML alert -- before committing to the request, not after a manager has already spent time reviewing it. A failing check is shown, not blocked: real hard rules are still enforced server-side at approve(). Guarded with a 5s timeout and a fallback message so a slow or unavailable check can never leave the dialog looking stuck. error-handler.interceptor.ts: a document/signature attachment whose row survives but whose file is missing from the content store now resolves to a silent 404 (matching the existing image-fetch handling) instead of surfacing a raw error. Background GET failures (500/501/503) on calls a component already recovers from (photo, attachment, notes, standing instructions, ...) now surface as a corner toast instead of a blocking modal that has to be dismissed -- a write action's failure (POST/PUT/DELETE) still gets the centred, must-acknowledge dialog, since that is the one thing the user was waiting on. Also surfaces the backend's real message on a 500 instead of a hardcoded generic sentence. Backend counterpart: ClerkShareWithdrawalRequestApiResource (preview-validate endpoint, shared buildShareWithdrawalChecks()) and DocumentReadPlatformServiceImpl (missing-file 404) land separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
483 lines
18 KiB
HTML
483 lines
18 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="pg">
|
|
<header class="pg-head">
|
|
<div>
|
|
<div class="pg-eyebrow">{{ isReview ? 'CHECKER · APPROVAL' : 'CLERK · SHARE WITHDRAWAL' }}</div>
|
|
<h1 class="pg-title">{{ pageTitle }}</h1>
|
|
<p class="pg-sub">{{ pageSubtitle }}</p>
|
|
</div>
|
|
<div class="pg-toolbar">
|
|
<button type="button" class="pg-btn" (click)="goBack()">
|
|
<mat-icon>arrow_back</mat-icon>
|
|
{{ isReview ? 'Back to Requests' : 'My Share Requests' }}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Share Account</div>
|
|
<div class="inv-form-grid" style="padding: 16px">
|
|
@if (!isReview) {
|
|
<label class="inv-form-field" style="grid-column: span 2">
|
|
<span>* Search Account No. / Client Name</span>
|
|
<mifosx-search-select
|
|
[options]="accountOptions"
|
|
[(ngModel)]="selectedAccountId"
|
|
(ngModelChange)="onAccountSelected($event)"
|
|
placeholder="Search share account no. or client name…"
|
|
></mifosx-search-select>
|
|
</label>
|
|
}
|
|
@if (account) {
|
|
<label class="inv-form-field">
|
|
<span>Account No</span>
|
|
<input type="text" [value]="account.accountNo" readonly />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Client</span>
|
|
<input type="text" [value]="account.clientName" readonly />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Shares Held</span>
|
|
<input
|
|
type="text"
|
|
[value]="account.summary?.totalApprovedShares ?? account.totalApprovedShares ?? 0"
|
|
readonly
|
|
/>
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Unit Price</span>
|
|
<input type="text" [value]="'₹' + (unitPrice | number: '1.2-2')" readonly />
|
|
</label>
|
|
}
|
|
@if (loadingAccount) {
|
|
<div class="pg-empty">Loading account…</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (isReview && reviewRequest) {
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Client Compliance</div>
|
|
<div class="inv-form-grid" style="padding: 16px">
|
|
<div class="inv-form-field">
|
|
<span>KYC Status</span>
|
|
<span
|
|
class="swr-chip"
|
|
[class.swr-chip--ok]="reviewRequest.client_kyc_verified"
|
|
[class.swr-chip--warn]="!reviewRequest.client_kyc_verified"
|
|
>
|
|
{{ reviewRequest.client_kyc_status_label }}
|
|
</span>
|
|
</div>
|
|
<div class="inv-form-field">
|
|
<span>Risk Category</span>
|
|
<span
|
|
class="swr-chip"
|
|
[class.swr-chip--ok]="!reviewRequest.client_risk_high"
|
|
[class.swr-chip--warn]="reviewRequest.client_risk_high"
|
|
>
|
|
{{ reviewRequest.client_risk_category_label }}
|
|
</span>
|
|
</div>
|
|
<div class="inv-form-field">
|
|
<span>Open AML Alerts</span>
|
|
<span
|
|
class="swr-chip"
|
|
[class.swr-chip--ok]="!reviewRequest.client_open_aml_alerts"
|
|
[class.swr-chip--warn]="reviewRequest.client_open_aml_alerts > 0"
|
|
>
|
|
{{ reviewRequest.client_open_aml_alerts || 0 }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (account) {
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Withdrawal Details</div>
|
|
<div class="inv-form-grid" style="padding: 16px">
|
|
<label class="inv-form-field">
|
|
<span>* Shares to Withdraw</span>
|
|
<input type="number" [(ngModel)]="requestedShares" min="1" [disabled]="isReview" />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>* Requested Date</span>
|
|
<input type="date" [(ngModel)]="requestedDate" [disabled]="isReview" />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Withdrawal Value</span>
|
|
<input type="text" [value]="'₹' + (withdrawalValue | number: '1.2-2')" readonly />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Remaining Shares After Withdrawal</span>
|
|
<input type="text" [value]="remainingShares" readonly />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>* Board Resolution Number</span>
|
|
<input type="text" [(ngModel)]="resolutionNumber" [disabled]="isReview" placeholder="e.g. BR-2026-014" />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>* Date of Meeting</span>
|
|
<input type="date" [(ngModel)]="meetingDate" [disabled]="isReview" />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>* Settlement Destination</span>
|
|
@if (!isReview) {
|
|
<select [ngModel]="settlementMode" (ngModelChange)="onSettlementModeChange($event)">
|
|
<option value="SAVINGS">Member's Linked Savings Account</option>
|
|
<option value="GL">GL Account</option>
|
|
</select>
|
|
} @else {
|
|
<input
|
|
type="text"
|
|
[value]="reviewRequest?.settlement_mode === 'GL' ? 'GL Account' : 'Linked Savings Account'"
|
|
readonly
|
|
/>
|
|
}
|
|
</label>
|
|
@if (!isReview && settlementMode === 'GL') {
|
|
<label class="inv-form-field">
|
|
<span>* Settlement GL Account</span>
|
|
<mifosx-gl-account-selector
|
|
[inputFormControl]="toGlAccountControl"
|
|
[glAccountList]="glAccountsData"
|
|
[required]="true"
|
|
></mifosx-gl-account-selector>
|
|
</label>
|
|
}
|
|
<label class="inv-form-field">
|
|
<span>* Settlement Payment Type</span>
|
|
@if (!isReview) {
|
|
<select [(ngModel)]="settlementPaymentTypeId">
|
|
<option [ngValue]="null">Select…</option>
|
|
@for (pt of paymentTypeOptions; track pt.id) {
|
|
<option [ngValue]="pt.id">{{ pt.name }}</option>
|
|
}
|
|
</select>
|
|
} @else {
|
|
<input type="text" [value]="settlementPaymentTypeName" readonly />
|
|
}
|
|
</label>
|
|
<label class="inv-form-field" style="grid-column: span 2">
|
|
<span>Description / Remarks</span>
|
|
<input type="text" [(ngModel)]="description" [disabled]="isReview" />
|
|
</label>
|
|
</div>
|
|
|
|
@if (account && remainingShares === 0) {
|
|
<div class="swr-close-notice">
|
|
<mat-icon>lock</mat-icon>
|
|
<span>
|
|
This withdraws <strong>all</strong> shares on this account. Approving it will
|
|
<strong>close the share account automatically</strong> — no separate closure step is needed.
|
|
</span>
|
|
</div>
|
|
}
|
|
|
|
@if (!isReview) {
|
|
<div class="inv-form-actions">
|
|
<button
|
|
type="button"
|
|
class="pg-btn pg-btn--primary"
|
|
[disabled]="!isFormValid || submitting"
|
|
(click)="openConfirmDialog()"
|
|
>
|
|
{{ submitting ? 'Submitting…' : 'Review & Submit' }}
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (isReview && reviewRequest?.status === 'PENDING') {
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Verification</div>
|
|
<div class="swrv-trigger">
|
|
<div class="swrv-trigger__icon">
|
|
<mat-icon>fact_check</mat-icon>
|
|
</div>
|
|
<div class="swrv-trigger__text">
|
|
<div class="swrv-trigger__title">Run a live backend validation</div>
|
|
<div class="swrv-trigger__sub">
|
|
Re-checks account status, share balance, linked savings account and GL mapping before you approve.
|
|
</div>
|
|
</div>
|
|
<button type="button" class="pg-btn pg-btn--primary" (click)="openValidateModal()">
|
|
<mat-icon>play_circle</mat-icon>
|
|
Validate Request
|
|
</button>
|
|
</div>
|
|
|
|
<div class="inv-form-actions">
|
|
<button type="button" class="pg-btn pg-btn--primary" (click)="approveReview()">
|
|
<mat-icon>check_circle</mat-icon>
|
|
Approve & Process Withdrawal
|
|
</button>
|
|
<button type="button" class="pg-btn pg-btn--danger" (click)="openRejectDialog()">
|
|
<mat-icon>cancel</mat-icon>
|
|
Reject
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (showValidateModal) {
|
|
<div class="swrv-modal">
|
|
<div class="swrv-modal__card">
|
|
<div class="swrv-modal__header">
|
|
<div class="swrv-modal__title">
|
|
<span class="swrv-modal__title-icon"><mat-icon>fact_check</mat-icon></span>
|
|
Validate Share Withdrawal Request
|
|
</div>
|
|
<button type="button" class="swrv-modal__close" [disabled]="validating" (click)="closeValidateModal()">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="swrv-modal__body">
|
|
<p class="swrv-modal__intro">
|
|
Re-checking <strong>{{ requestedShares }} share(s)</strong> withdrawal from
|
|
<strong>{{ account?.accountNo }}</strong> ({{ account?.clientName }}) against the live account, savings link
|
|
and GL configuration — not just what was captured at submission time.
|
|
</p>
|
|
|
|
@if (backendChecks && !validating && !validateAllPassed) {
|
|
<div class="swrv-modal__warn">
|
|
<mat-icon>warning</mat-icon>
|
|
<div>
|
|
<strong>Some checks need review before approving.</strong>
|
|
Scroll down to see which ones failed and why.
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<ol class="swrv-steps">
|
|
@for (step of validateSteps; track step.label; let i = $index) {
|
|
<li
|
|
class="swrv-step"
|
|
[class.swrv-step--active]="step.status === 'active'"
|
|
[class.swrv-step--done]="step.status === 'done'"
|
|
[class.swrv-step--failed]="step.status === 'failed'"
|
|
>
|
|
<div class="swrv-step__indicator">
|
|
@if (step.status === 'done') {
|
|
<mat-icon>check</mat-icon>
|
|
} @else if (step.status === 'active') {
|
|
<mat-icon class="swrv-spin">autorenew</mat-icon>
|
|
} @else if (step.status === 'failed') {
|
|
<mat-icon>close</mat-icon>
|
|
} @else {
|
|
<span>{{ i + 1 }}</span>
|
|
}
|
|
</div>
|
|
<div class="swrv-step__content">
|
|
<div class="swrv-step__label">{{ step.label }}</div>
|
|
<div class="swrv-step__msg">{{ step.detail }}</div>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ol>
|
|
|
|
@if (validating && validateSteps.length === 0) {
|
|
<div class="pg-empty">Connecting to server…</div>
|
|
}
|
|
|
|
@if (backendChecks && !validating && validateAllPassed) {
|
|
<div class="swrv-modal__result">
|
|
<mat-icon>verified</mat-icon>
|
|
<span>All checks passed — safe to approve.</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="swrv-modal__footer">
|
|
@if (!validating && backendChecks) {
|
|
<button type="button" class="pg-btn" (click)="runBackendValidation()">
|
|
<mat-icon>refresh</mat-icon>
|
|
Re-run
|
|
</button>
|
|
}
|
|
@if (!validating && backendChecks && validateAllPassed) {
|
|
<button type="button" class="pg-btn pg-btn--primary" (click)="closeValidateModal(); approveReview()">
|
|
<mat-icon>check_circle</mat-icon>
|
|
Approve & Process Withdrawal
|
|
</button>
|
|
}
|
|
<button type="button" class="pg-btn" [disabled]="validating" (click)="closeValidateModal()">
|
|
{{ validating ? 'Validating…' : 'Close' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (isReview && reviewRequest?.status === 'APPROVED') {
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Settlement</div>
|
|
<div class="inv-form-grid" style="padding: 16px">
|
|
@if (reviewRequest?.settlement_amount) {
|
|
<label class="inv-form-field">
|
|
<span>Amount Credited</span>
|
|
<input type="text" [value]="'₹' + (reviewRequest.settlement_amount | number: '1.2-2')" readonly />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Credited To (Linked Savings A/C)</span>
|
|
<input type="text" [value]="account?.savingsAccountNumber || '—'" readonly />
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Savings Transaction Ref.</span>
|
|
<input type="text" [value]="reviewRequest.settlement_savings_transaction_id" readonly />
|
|
</label>
|
|
} @else {
|
|
<div class="pg-empty" style="grid-column: span 2">
|
|
Settlement is processed automatically on approval. Details aren't available when reopening an
|
|
already-approved request — check the linked savings account's transaction history to confirm.
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (isReview && activityLog.length > 0) {
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Activity Log</div>
|
|
<table class="pg-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Timestamp</th>
|
|
<th>Action</th>
|
|
<th>User</th>
|
|
<th>Note</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (entry of activityLog; track entry.timestamp) {
|
|
<tr>
|
|
<td>{{ entry.timestamp | date: 'medium' }}</td>
|
|
<td>{{ entry.action }}</td>
|
|
<td>{{ entry.user }}</td>
|
|
<td>{{ entry.note || '—' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
@if (showConfirmDialog) {
|
|
<div class="swr-modal-backdrop">
|
|
<div class="swr-modal">
|
|
<h3>Confirm Share Withdrawal Request</h3>
|
|
<p class="pg-sub">Review the details below before submitting for Bank Manager approval.</p>
|
|
<table class="pg-table">
|
|
<tbody>
|
|
<tr>
|
|
<td>Account No / Client</td>
|
|
<td>{{ account?.accountNo }} — {{ account?.clientName }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Shares to Withdraw</td>
|
|
<td>
|
|
{{ requestedShares }} @ ₹{{ unitPrice | number: '1.2-2' }} = ₹{{ withdrawalValue | number: '1.2-2' }}
|
|
</td>
|
|
</tr>
|
|
<tr [class.swr-close-row]="remainingShares === 0">
|
|
<td>Remaining Shares After Withdrawal</td>
|
|
<td>
|
|
{{ remainingShares }}
|
|
@if (remainingShares === 0) {
|
|
<span class="swr-close-tag"><mat-icon>lock</mat-icon> Account will be closed on approval</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Requested Date</td>
|
|
<td>{{ requestedDate }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Board Resolution Number</td>
|
|
<td>{{ resolutionNumber }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Date of Meeting</td>
|
|
<td>{{ meetingDate }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Settlement Destination</td>
|
|
<td>{{ settlementMode === 'GL' ? 'GL Account' : 'Linked Savings Account' }}</td>
|
|
</tr>
|
|
@if (settlementMode === 'GL') {
|
|
<tr>
|
|
<td>Settlement GL Account</td>
|
|
<td>{{ selectedSettlementGlName }}</td>
|
|
</tr>
|
|
} @else {
|
|
<tr>
|
|
<td>Linked Savings Account</td>
|
|
<td>{{ account?.savingsAccountNumber || 'None — settlement will not run automatically' }}</td>
|
|
</tr>
|
|
}
|
|
<tr>
|
|
<td>Settlement Payment Type</td>
|
|
<td>{{ settlementPaymentTypeName }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="pos-section-label" style="margin-top: 12px">Validation Checks</div>
|
|
<table class="pg-table">
|
|
<tbody>
|
|
@for (check of verificationChecks; track check.label) {
|
|
<tr>
|
|
<td>{{ check.label }}</td>
|
|
<td [class.swr-check-pass]="check.pass" [class.swr-check-fail]="!check.pass">
|
|
{{ check.pass ? 'Pass — ' : 'Review — ' }}{{ check.detail }}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="inv-form-actions">
|
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="submitting" (click)="confirmSubmit()">
|
|
{{ submitting ? 'Submitting…' : 'Confirm & Submit' }}
|
|
</button>
|
|
<button type="button" class="pg-btn" (click)="cancelConfirmDialog()">Back to Edit</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (showRejectDialog) {
|
|
<div class="swr-modal-backdrop">
|
|
<div class="swr-modal">
|
|
<h3>Reject Share Withdrawal Request</h3>
|
|
<label class="inv-form-field">
|
|
<span>* Rejection Reason</span>
|
|
<textarea rows="3" [(ngModel)]="rejectionReason"></textarea>
|
|
</label>
|
|
<div class="inv-form-actions">
|
|
<button
|
|
type="button"
|
|
class="pg-btn pg-btn--danger"
|
|
[disabled]="!rejectionReason.trim()"
|
|
(click)="confirmRejectReview()"
|
|
>
|
|
Confirm Reject
|
|
</button>
|
|
<button type="button" class="pg-btn" (click)="showRejectDialog = false">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|