Replaces the one-click Close action on the savings account page (removed from the Danger Zone menu) with a maker-checker flow, since closing an account pays real cash out of a teller's drawer. Clerk: an Action button on Transaction Entry, shown once an ACTIVE savings account is loaded. Ticking "Close this account" previews the payout -- balance plus interest calculated to today by the same code the closure itself runs -- and Save drops that figure into the slip's AMOUNT. The clerk then fills TOK. NO. on the slip and presses Save, which raises the request. Token and narration deliberately live on the slip, not in the popup, so the closure is entered like any other transaction. Manager: a Closure Requests tab in the Approval Center, reviewed in a popup dialog (matching Disbursement Requests) rather than a separate route. Cashier: the closure token is recognised at the counter alongside withdrawal and dividend tokens, auto-filling the rounded cash figure, locking it, and closing the account once the denomination is counted. Save-path fixes, all of which silently blocked a valid closure: - amount came from the denomination total whenever clerkMode was false -- which it is for any route other than the Clerk Dashboard wrapper -- so the calculated payout was replaced by zero - the balance check rejected the payout because it legitimately exceeds today's withdrawable amount: it includes interest close() posts before withdrawing. Min-balance, withdrawal-facility and clerk-side denomination checks do not apply to a closure either - on the cashier side the account is already CLOSED, so accountDetails is a stub with no balance and the same check reported "Withdrawable 0.00" - the closure branch had to precede the plain clerk-withdrawal branch: a closure is a full-balance withdrawal, so that branch would have swallowed it and the account would never have closed - onSave's one silent return now says what is missing, instead of leaving Save looking like a broken button Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
113 lines
3.2 KiB
TypeScript
113 lines
3.2 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 { RouterModule } from '@angular/router';
|
|
import { catchError } from 'rxjs/operators';
|
|
import { of } from 'rxjs';
|
|
|
|
import { ClerkClosureRequestService } from '../../cashier/clerk-closure-request.service';
|
|
|
|
/** Clerk Dashboard "My Closure Requests" — lists the Cash account-closure requests this clerk has
|
|
* submitted (from the account's own Transaction Entry screen) and their approval/disbursement
|
|
* status. Mirrors MyShareWithdrawalRequestsComponent. There is no "New Closure Request" button
|
|
* here -- submission happens in-context on the account's own screen, not from this list. */
|
|
@Component({
|
|
selector: 'mifosx-my-closure-requests',
|
|
templateUrl: './my-closure-requests.component.html',
|
|
styleUrls: [
|
|
'../../cashier/cashier-shared.scss',
|
|
'../clerk-shared.scss'
|
|
],
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
RouterModule
|
|
]
|
|
})
|
|
export class MyClosureRequestsComponent implements OnInit {
|
|
private service = inject(ClerkClosureRequestService);
|
|
|
|
requests: any[] = [];
|
|
totalRecords = 0;
|
|
loading = false;
|
|
loaded = false;
|
|
|
|
filterStatus = '';
|
|
|
|
get statusOptions(): { value: string; label: string }[] {
|
|
return [
|
|
{ value: '', label: 'All' },
|
|
{ value: 'PENDING', label: 'Pending' },
|
|
{ value: 'APPROVED', label: 'Approved — awaiting cashier' },
|
|
{ value: 'REJECTED', label: 'Rejected' },
|
|
{ value: 'CLOSED', label: 'Closed' }
|
|
];
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.load();
|
|
}
|
|
|
|
load(): void {
|
|
this.loading = true;
|
|
this.service
|
|
.getMyRequests(this.filterStatus || undefined, 0, 50)
|
|
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
|
.subscribe((res: any) => {
|
|
this.requests = res?.pageItems ?? [];
|
|
this.totalRecords = Number(res?.totalFilteredRecords ?? 0);
|
|
this.loading = false;
|
|
this.loaded = true;
|
|
});
|
|
}
|
|
|
|
onFilterChange(): void {
|
|
this.load();
|
|
}
|
|
|
|
statusClass(status: string): string {
|
|
const map: Record<string, string> = {
|
|
PENDING: 'inv-badge--active',
|
|
APPROVED: 'inv-badge--matured',
|
|
REJECTED: 'inv-badge--written_off',
|
|
CLOSED: 'inv-badge--matured'
|
|
};
|
|
return `inv-badge ${map[status] ?? ''}`;
|
|
}
|
|
|
|
statusLabel(status: string): string {
|
|
const map: Record<string, string> = {
|
|
PENDING: 'Pending Approval',
|
|
APPROVED: 'Awaiting Cashier',
|
|
REJECTED: 'Rejected',
|
|
CLOSED: 'Closed'
|
|
};
|
|
return map[status] ?? status;
|
|
}
|
|
|
|
formatDateTime(val: any): string {
|
|
if (!val) return '—';
|
|
try {
|
|
const d = new Date(val);
|
|
return d.toLocaleString('en-IN', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
} catch {
|
|
return String(val);
|
|
}
|
|
}
|
|
}
|