/** * 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/. */ import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; import { CashierService } from '../../../cashier/cashier.service'; /** * Approval Center tab — share applications a clerk has asked to settle out of the member's own * savings, awaiting a manager. * * Approving is the moment the member's balance actually changes: it withdraws the share value plus * entry fee from their savings and issues the shares. Nothing has moved before that, which is why * this step exists at all — the cash route needs no checker because the cashier's receipt is * already a second pair of eyes, whereas here nobody has counted anything. * * A manager cannot approve a request they raised themselves; the server rejects that, and the * message is surfaced on the row rather than swallowed. */ @Component({ selector: 'mifosx-share-savings-debit-requests', templateUrl: './share-savings-debit-requests.component.html', styleUrls: [ '../../../cashier/cashier-shared.scss', '../../../clerk/clerk-shared.scss', './share-savings-debit-requests.component.scss' ], standalone: true, imports: [ CommonModule, FormsModule ] }) export class ShareSavingsDebitRequestsComponent implements OnInit { private cashierService = inject(CashierService); rows: any[] = []; loading = false; loaded = false; actioningId: number | null = null; actionError: Record = {}; /** Which row has its reject box open, and what reason has been typed into it. */ expandedRejectId: number | null = null; rejectReason: Record = {}; ngOnInit(): void { this.load(); } load(): void { this.loading = true; this.cashierService .getShareSavingsDebitRequests('PENDING') .pipe(catchError(() => of([]))) .subscribe((res: any) => { this.rows = res ?? []; this.loading = false; this.loaded = true; }); } approve(row: any): void { this.actioningId = row.id; delete this.actionError[row.id]; this.cashierService.approveShareSavingsDebit(row.id).subscribe({ next: () => { this.actioningId = null; this.load(); }, error: (err: any) => { this.actioningId = null; this.actionError[row.id] = err?.error?.defaultUserMessage || err?.error?.message || 'Could not approve this request.'; } }); } toggleReject(row: any): void { this.expandedRejectId = this.expandedRejectId === row.id ? null : row.id; } reject(row: any): void { this.actioningId = row.id; delete this.actionError[row.id]; this.cashierService.rejectShareSavingsDebit(row.id, this.rejectReason[row.id]?.trim()).subscribe({ next: () => { this.actioningId = null; this.expandedRejectId = null; this.load(); }, error: (err: any) => { this.actioningId = null; this.actionError[row.id] = err?.error?.defaultUserMessage || err?.error?.message || 'Could not reject this request.'; } }); } /** * Whether the savings account still covers what the request asks for. * * The balance can move between a clerk raising the request and a manager reaching it, and the * server refuses a short account — showing it on the row means the manager sees why before * clicking rather than after. */ isShort(row: any): boolean { return Number(row.savings_balance ?? 0) < Number(row.total_amount ?? 0); } }