121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
/**
|
|
* 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<number, string> = {};
|
|
|
|
/** Which row has its reject box open, and what reason has been typed into it. */
|
|
expandedRejectId: number | null = null;
|
|
rejectReason: Record<number, string> = {};
|
|
|
|
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);
|
|
}
|
|
}
|