Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora into dev-latest
This commit is contained in:
@@ -51,6 +51,12 @@ export class ClerkShareWithdrawalService {
|
|||||||
return this.http.post(`/clerk/share-withdrawal-requests/${id}/validate`, {});
|
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<any> {
|
||||||
|
return this.http.post('/clerk/share-withdrawal-requests/preview-validate', data);
|
||||||
|
}
|
||||||
|
|
||||||
approve(id: number): Observable<any> {
|
approve(id: number): Observable<any> {
|
||||||
return this.http.post(`/clerk/share-withdrawal-requests/${id}/approve`, {});
|
return this.http.post(`/clerk/share-withdrawal-requests/${id}/approve`, {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
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 { forkJoin, of, Subject } from 'rxjs';
|
||||||
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -3426,6 +3426,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
meetingDate: shareRow.meetingDate,
|
meetingDate: shareRow.meetingDate,
|
||||||
description: shareRow.description || null,
|
description: shareRow.description || null,
|
||||||
settlementPaymentTypeId: savingsRow.paymentTypeId,
|
settlementPaymentTypeId: savingsRow.paymentTypeId,
|
||||||
|
settlementMode: 'SAVINGS',
|
||||||
settlementRowSnapshot
|
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, settlement and account closure all post together on approval.'
|
||||||
: 'Pending Manager approval — redemption and settlement both 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)
|
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<string, unknown>,
|
||||||
|
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
|
* 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
|
* 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, settlement and account closure all post together on approval.'
|
||||||
: 'Pending Manager approval — redemption and settlement both 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)
|
this.doSubmitShareGlSettlementTransfer(payload, shareRow, shares, glRow.acNo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,31 @@
|
|||||||
<span>* Date of Meeting</span>
|
<span>* Date of Meeting</span>
|
||||||
<input type="date" [(ngModel)]="meetingDate" [disabled]="isReview" />
|
<input type="date" [(ngModel)]="meetingDate" [disabled]="isReview" />
|
||||||
</label>
|
</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">
|
<label class="inv-form-field">
|
||||||
<span>* Settlement Payment Type</span>
|
<span>* Settlement Payment Type</span>
|
||||||
@if (!isReview) {
|
@if (!isReview) {
|
||||||
@@ -387,13 +412,24 @@
|
|||||||
<td>{{ meetingDate }}</td>
|
<td>{{ meetingDate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Settlement Payment Type</td>
|
<td>Settlement Destination</td>
|
||||||
<td>{{ settlementPaymentTypeName }}</td>
|
<td>{{ settlementMode === 'GL' ? 'GL Account' : 'Linked Savings Account' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@if (settlementMode === 'GL') {
|
||||||
|
<tr>
|
||||||
|
<td>Settlement GL Account</td>
|
||||||
|
<td>{{ selectedSettlementGlName }}</td>
|
||||||
|
</tr>
|
||||||
|
} @else {
|
||||||
<tr>
|
<tr>
|
||||||
<td>Linked Savings Account</td>
|
<td>Linked Savings Account</td>
|
||||||
<td>{{ account?.savingsAccountNumber || 'None — settlement will not run automatically' }}</td>
|
<td>{{ account?.savingsAccountNumber || 'None — settlement will not run automatically' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td>Settlement Payment Type</td>
|
||||||
|
<td>{{ settlementPaymentTypeName }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule, UntypedFormControl } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
@@ -19,6 +19,9 @@ import { SharesService } from '../../shares/shares.service';
|
|||||||
import { ClerkShareWithdrawalService } from '../../cashier/clerk-share-withdrawal.service';
|
import { ClerkShareWithdrawalService } from '../../cashier/clerk-share-withdrawal.service';
|
||||||
import { CashierService } from '../../cashier/cashier.service';
|
import { CashierService } from '../../cashier/cashier.service';
|
||||||
import { SearchSelectComponent, SearchSelectOption } from '../../shared/search-select/search-select.component';
|
import { SearchSelectComponent, SearchSelectOption } from '../../shared/search-select/search-select.component';
|
||||||
|
import { GlAccountSelectorComponent } from '../../shared/accounting/gl-account-selector/gl-account-selector.component';
|
||||||
|
import { AccountingService } from '../../accounting/accounting.service';
|
||||||
|
import { GLAccount } from '../../shared/models/general.model';
|
||||||
|
|
||||||
export type ShareWdMode = 'create' | 'review';
|
export type ShareWdMode = 'create' | 'review';
|
||||||
|
|
||||||
@@ -51,7 +54,8 @@ interface ActivityEntry {
|
|||||||
RouterModule,
|
RouterModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
SearchSelectComponent
|
SearchSelectComponent,
|
||||||
|
GlAccountSelectorComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ShareWithdrawalRequestComponent implements OnInit {
|
export class ShareWithdrawalRequestComponent implements OnInit {
|
||||||
@@ -60,6 +64,7 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
private sharesService = inject(SharesService);
|
private sharesService = inject(SharesService);
|
||||||
private wdService = inject(ClerkShareWithdrawalService);
|
private wdService = inject(ClerkShareWithdrawalService);
|
||||||
private cashierService = inject(CashierService);
|
private cashierService = inject(CashierService);
|
||||||
|
private accountingService = inject(AccountingService);
|
||||||
private snackBar = inject(MatSnackBar);
|
private snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
/** When embedded (e.g. opened as a popup from a list screen instead of routed to directly), mode
|
/** When embedded (e.g. opened as a popup from a list screen instead of routed to directly), mode
|
||||||
@@ -91,6 +96,17 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
paymentTypes: any[] = [];
|
paymentTypes: any[] = [];
|
||||||
settlementPaymentTypeId: number | null = null;
|
settlementPaymentTypeId: number | null = null;
|
||||||
|
|
||||||
|
/** Where the redeemed value goes. 'SAVINGS' (the long-standing default) credits the member's
|
||||||
|
* linked savings account. 'GL' lets the clerk pick any postable GL account directly — same
|
||||||
|
* option the Cashier's Transaction Entry screen already offers on its CR row, now available
|
||||||
|
* here too so both entry points behave the same way. Naming the product's own Share Sundry GL
|
||||||
|
* here is still fine: the backend recognises it and books straight to Sundry with no further
|
||||||
|
* settlement leg (see ClerkShareWithdrawalRequestApiResource#submit). */
|
||||||
|
settlementMode: 'SAVINGS' | 'GL' = 'SAVINGS';
|
||||||
|
settlementGlAccountId: number | null = null;
|
||||||
|
glAccountsData: GLAccount[] = [];
|
||||||
|
toGlAccountControl = new UntypedFormControl('');
|
||||||
|
|
||||||
activityLog: ActivityEntry[] = [];
|
activityLog: ActivityEntry[] = [];
|
||||||
reviewRequest: any = null;
|
reviewRequest: any = null;
|
||||||
showRejectDialog = false;
|
showRejectDialog = false;
|
||||||
@@ -132,6 +148,17 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
return this.selectedSettlementPaymentType?.name || '—';
|
return this.selectedSettlementPaymentType?.name || '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The chosen GL account's display name, for the confirm dialog and the checklist. Falls back to
|
||||||
|
* the raw id while the GL account list is still loading. */
|
||||||
|
get selectedSettlementGlName(): string {
|
||||||
|
if (!this.settlementGlAccountId) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.glAccountsData.find((g) => g.id === this.settlementGlAccountId)?.name || `GL #${this.settlementGlAccountId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get isFormValid(): boolean {
|
get isFormValid(): boolean {
|
||||||
if (!this.account) return false;
|
if (!this.account) return false;
|
||||||
if (!this.requestedShares || this.requestedShares <= 0) return false;
|
if (!this.requestedShares || this.requestedShares <= 0) return false;
|
||||||
@@ -141,6 +168,7 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
if (!this.resolutionNumber.trim()) return false;
|
if (!this.resolutionNumber.trim()) return false;
|
||||||
if (!this.meetingDate) return false;
|
if (!this.meetingDate) return false;
|
||||||
if (!this.settlementPaymentTypeId) return false;
|
if (!this.settlementPaymentTypeId) return false;
|
||||||
|
if (this.settlementMode === 'GL' && !this.settlementGlAccountId) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +188,17 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
pass: shares > 0 && shares <= held,
|
pass: shares > 0 && shares <= held,
|
||||||
detail: `${shares} of ${held} shares held`
|
detail: `${shares} of ${held} shares held`
|
||||||
},
|
},
|
||||||
{
|
this.settlementMode === 'GL'
|
||||||
|
? {
|
||||||
|
label: 'Settlement GL Account selected',
|
||||||
|
pass: !!this.settlementGlAccountId,
|
||||||
|
detail: this.settlementGlAccountId ? this.selectedSettlementGlName : 'Missing'
|
||||||
|
}
|
||||||
|
: {
|
||||||
label: 'Linked savings account confirmed',
|
label: 'Linked savings account confirmed',
|
||||||
pass: !!this.account?.savingsAccountNumber,
|
pass: !!this.account?.savingsAccountNumber,
|
||||||
detail: this.account?.savingsAccountNumber || 'No linked savings account — settlement cannot run automatically'
|
detail:
|
||||||
|
this.account?.savingsAccountNumber || 'No linked savings account — settlement cannot run automatically'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Settlement Payment Type selected (non-cash)',
|
label: 'Settlement Payment Type selected (non-cash)',
|
||||||
@@ -214,11 +249,37 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
.pipe(catchError(() => of([])))
|
.pipe(catchError(() => of([])))
|
||||||
.subscribe((types: any[]) => (this.paymentTypes = types ?? []));
|
.subscribe((types: any[]) => (this.paymentTypes = types ?? []));
|
||||||
|
|
||||||
|
this.toGlAccountControl.valueChanges.subscribe((id: any) => {
|
||||||
|
this.settlementGlAccountId = id ? Number(id) : null;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.requestId && this.mode === 'review') {
|
if (this.requestId && this.mode === 'review') {
|
||||||
this.loadReviewRequest(this.requestId);
|
this.loadReviewRequest(this.requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Flips the settlement destination. Switching away from GL clears whatever was picked so a
|
||||||
|
* stale GL id can never ride along on a SAVINGS submission. */
|
||||||
|
onSettlementModeChange(mode: 'SAVINGS' | 'GL'): void {
|
||||||
|
this.settlementMode = mode;
|
||||||
|
if (mode === 'GL') {
|
||||||
|
this.loadGlAccounts();
|
||||||
|
} else {
|
||||||
|
this.settlementGlAccountId = null;
|
||||||
|
this.toGlAccountControl.setValue('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetched on first use rather than with the page, since most withdrawals settle to savings. */
|
||||||
|
private loadGlAccounts(): void {
|
||||||
|
if (this.glAccountsData.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.accountingService.getGlAccounts().subscribe((accounts: GLAccount[]) => {
|
||||||
|
this.glAccountsData = accounts ?? [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onAccountSelected(accountId: string): void {
|
onAccountSelected(accountId: string): void {
|
||||||
this.selectedAccountId = accountId;
|
this.selectedAccountId = accountId;
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
@@ -246,6 +307,11 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
this.resolutionNumber = req.resolution_number ?? '';
|
this.resolutionNumber = req.resolution_number ?? '';
|
||||||
this.meetingDate = req.meeting_date ? String(req.meeting_date).split('T')[0].split(' ')[0] : this.meetingDate;
|
this.meetingDate = req.meeting_date ? String(req.meeting_date).split('T')[0].split(' ')[0] : this.meetingDate;
|
||||||
this.description = req.description ?? '';
|
this.description = req.description ?? '';
|
||||||
|
this.settlementMode = req.settlement_mode === 'GL' ? 'GL' : 'SAVINGS';
|
||||||
|
this.settlementGlAccountId = req.settlement_gl_account_id ? Number(req.settlement_gl_account_id) : null;
|
||||||
|
if (this.settlementMode === 'GL') {
|
||||||
|
this.loadGlAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
if (req.activity_log) {
|
if (req.activity_log) {
|
||||||
try {
|
try {
|
||||||
@@ -289,7 +355,9 @@ export class ShareWithdrawalRequestComponent implements OnInit {
|
|||||||
resolutionNumber: this.resolutionNumber.trim(),
|
resolutionNumber: this.resolutionNumber.trim(),
|
||||||
meetingDate: this.meetingDate,
|
meetingDate: this.meetingDate,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
settlementPaymentTypeId: this.settlementPaymentTypeId
|
settlementPaymentTypeId: this.settlementPaymentTypeId,
|
||||||
|
settlementMode: this.settlementMode,
|
||||||
|
...(this.settlementMode === 'GL' ? { settlementGlAccountId: this.settlementGlAccountId } : {})
|
||||||
};
|
};
|
||||||
this.wdService.submit(payload).subscribe({
|
this.wdService.submit(payload).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
|||||||
@@ -67,12 +67,18 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
// endpoint isn't available on a given backend deployment, the client-general-step
|
// endpoint isn't available on a given backend deployment, the client-general-step
|
||||||
// component already falls back to riskRules = [] silently; no toast needed here either.
|
// component already falls back to riskRules = [] silently; no toast needed here either.
|
||||||
const isKycRiskRulesRequest = request.url.includes('/kyc/risk-rules');
|
const isKycRiskRulesRequest = request.url.includes('/kyc/risk-rules');
|
||||||
|
// A document's own file can be missing from the content store (a database copied between
|
||||||
|
// environments without its file store is the usual cause) even though its m_document row
|
||||||
|
// survives — the backend reports that as a 404, same as a client with no photo, and every
|
||||||
|
// caller that fetches a signature/attachment image already recovers with its own catchError.
|
||||||
|
const isDocumentAttachmentRequest = request.url.match(/\/documents\/\d+\/attachment$/);
|
||||||
const isSilentRequest =
|
const isSilentRequest =
|
||||||
isClientImageRequest ||
|
isClientImageRequest ||
|
||||||
isClientKycRequest ||
|
isClientKycRequest ||
|
||||||
isDirectorRequest ||
|
isDirectorRequest ||
|
||||||
isClientPendingEditRequest ||
|
isClientPendingEditRequest ||
|
||||||
isKycRiskRulesRequest;
|
isKycRiskRulesRequest ||
|
||||||
|
isDocumentAttachmentRequest;
|
||||||
|
|
||||||
// Explicit per-request opt-out (e.g. per-row enrichment fetches on a list page whose
|
// Explicit per-request opt-out (e.g. per-row enrichment fetches on a list page whose
|
||||||
// component already recovers via its own catchError) — silenced for every status, since
|
// component already recovers via its own catchError) — silenced for every status, since
|
||||||
@@ -93,8 +99,8 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolved once and emitted in one place below, so every failure here is consistently tagged
|
// Resolved once and emitted in one place below — see the severity decision there for which of
|
||||||
// as an error and therefore surfaces as a centred modal rather than a corner toast.
|
// these surfaces as a centred modal versus a corner toast.
|
||||||
let alert: { type: string; message: string } | null = null;
|
let alert: { type: string; message: string } | null = null;
|
||||||
if (status === 401 || (environment.oauth.enabled && status === 400)) {
|
if (status === 401 || (environment.oauth.enabled && status === 400)) {
|
||||||
alert = { type: 'Authentication Error', message: 'Invalid User Details. Please try again!' };
|
alert = { type: 'Authentication Error', message: 'Invalid User Details. Please try again!' };
|
||||||
@@ -119,7 +125,13 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
message: errorMessage || 'This action is not allowed in the current state.'
|
message: errorMessage || 'This action is not allowed in the current state.'
|
||||||
};
|
};
|
||||||
} else if (status === 500) {
|
} else if (status === 500) {
|
||||||
alert = { type: 'Internal Server Error', message: 'Internal Server Error. Please try again later.' };
|
// errorMessage carries the backend's actual explanation (defaultUserMessage/developerMessage)
|
||||||
|
// when one is present; the generic sentence is only a last resort for a response with no body
|
||||||
|
// at all (a proxy timeout, a stack trace the server never wrapped into JSON).
|
||||||
|
alert = {
|
||||||
|
type: 'Internal Server Error',
|
||||||
|
message: errorMessage || 'Internal Server Error. Please try again later.'
|
||||||
|
};
|
||||||
} else if (status === 501) {
|
} else if (status === 501) {
|
||||||
alert = {
|
alert = {
|
||||||
type: this.translate.instant('error.resource.notImplemented.type'),
|
type: this.translate.instant('error.resource.notImplemented.type'),
|
||||||
@@ -133,7 +145,17 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (alert) {
|
if (alert) {
|
||||||
this.alertService.alert({ ...alert, severity: 'error' });
|
// A GET is the page reading data it did not ask the user for, in the background — a photo,
|
||||||
|
// an attachment, a notes list, a standing-instructions panel. Every one of those already has
|
||||||
|
// its own catchError fallback at the call site (see transaction-entry.component.ts and
|
||||||
|
// similar account-overview loaders), so the page keeps working regardless. Blocking the whole
|
||||||
|
// screen on a dialog the user has to click "OK" on, for data that was never more than a
|
||||||
|
// secondary panel, is the "irritate na ho" complaint this guards against — it goes to the
|
||||||
|
// corner toast instead, same place a non-error alert already goes. A write (POST/PUT/PATCH/
|
||||||
|
// DELETE) is different: that is the one action the user was waiting on, so its failure still
|
||||||
|
// needs the centred, must-acknowledge dialog.
|
||||||
|
const isBackgroundRead = request.method === 'GET' && (status === 500 || status === 501 || status === 503);
|
||||||
|
this.alertService.alert({ ...alert, severity: isBackgroundRead ? undefined : 'error' });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw response;
|
throw response;
|
||||||
|
|||||||
Reference in New Issue
Block a user