feat(shares): add Share Account Closure maker-checker workflow
Clerk submits a closure request only once a share account holds zero shares; manager validates (shares still zero, account active, no active loan/guarantee, KYC/AML standing, maker!=checker) and approves, which executes the real Fineract "close" command. The direct native "Close" action is removed from the account menu — it silently force-redeemed any remaining shares, bypassing the withdrawal approval flow entirely — and replaced with an "Eligible for Closure" banner. When a withdrawal approval brings a share account's holding to zero, a closure request is now auto-submitted (still requires manager approval) so the clerk doesn't have to separately open "New Share Closure Request". Also fixes an unrelated build break introduced by a merged commit: a missing `suppressMonthlyAnnualLink` property declaration, and two files (branch-scope.service.ts, gl-reports-data.ts) that existed on `demo` but were never brought into `dev` alongside the new TDS report components that depend on them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
<!--
|
||||
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-card">
|
||||
<div class="pos-section-label">Share Closure Requests — Review, Approve or Reject</div>
|
||||
<div class="inv-form-grid" style="padding: 12px 16px">
|
||||
<label class="inv-form-field">
|
||||
<span>Status</span>
|
||||
<select [(ngModel)]="filterStatus" (change)="onFilterChange()">
|
||||
@for (opt of statusOptions; track opt.value) {
|
||||
<option [value]="opt.value">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field" style="align-self: end">
|
||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">Refresh</button>
|
||||
</label>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Client</th>
|
||||
<th>Closed Date</th>
|
||||
<th>Clerk</th>
|
||||
<th>Manager</th>
|
||||
<th>Submitted At</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading) {
|
||||
<tr>
|
||||
<td colspan="8" class="pg-empty">Loading…</td>
|
||||
</tr>
|
||||
} @else if (loaded && requests.length === 0) {
|
||||
<tr>
|
||||
<td colspan="8" class="pg-empty">No {{ filterStatus | lowercase }} share closure requests.</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (r of requests; track r.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.account_no }}</span>
|
||||
</td>
|
||||
<td>{{ r.client_name }}</td>
|
||||
<td>{{ formatDateTime(r.closed_date) }}</td>
|
||||
<td>{{ r.clerk_name }}</td>
|
||||
<td>{{ r.manager_name || r.manager_username || '—' }}</td>
|
||||
<td>{{ formatDateTime(r.submitted_at) }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="review(r)">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (showReviewModal && reviewRequestId) {
|
||||
<div class="swr-list-popup-backdrop" (click)="closeReviewModal()">
|
||||
<div class="swr-list-popup-panel" (click)="$event.stopPropagation()">
|
||||
<mifosx-share-closure-request
|
||||
[embedded]="true"
|
||||
[overrideMode]="'review'"
|
||||
[overrideRequestId]="reviewRequestId"
|
||||
(closeRequested)="closeReviewModal()"
|
||||
(actionCompleted)="onReviewActionCompleted()"
|
||||
></mifosx-share-closure-request>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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 { ClerkShareClosureService } from '../../../cashier/clerk-share-closure.service';
|
||||
import { ShareClosureRequestComponent } from '../../../clerk/share-closure-request/share-closure-request.component';
|
||||
|
||||
/** Approval Center — Share Closure Requests tab. A Bank Manager (checker) reviews, approves or
|
||||
* rejects share account closure requests submitted from the Clerk Dashboard. Mirrors
|
||||
* ShareWithdrawalRequestsComponent. Clicking "Review" opens the review screen in a popup on top of
|
||||
* this list, rather than navigating away, so approving/rejecting drops the manager straight back
|
||||
* into the (now refreshed) list. */
|
||||
@Component({
|
||||
selector: 'mifosx-share-closure-requests',
|
||||
templateUrl: './share-closure-requests.component.html',
|
||||
styleUrls: [
|
||||
'../../../cashier/cashier-shared.scss',
|
||||
'../../../clerk/clerk-shared.scss',
|
||||
'../share-withdrawal-requests/share-withdrawal-requests.component.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ShareClosureRequestComponent
|
||||
]
|
||||
})
|
||||
export class ShareClosureRequestsComponent implements OnInit {
|
||||
private service = inject(ClerkShareClosureService);
|
||||
|
||||
requests: any[] = [];
|
||||
totalRecords = 0;
|
||||
loading = false;
|
||||
loaded = false;
|
||||
|
||||
filterStatus = 'PENDING';
|
||||
|
||||
showReviewModal = false;
|
||||
reviewRequestId: number | null = null;
|
||||
|
||||
get statusOptions(): { value: string; label: string }[] {
|
||||
return [
|
||||
{ value: 'PENDING', label: 'Pending' },
|
||||
{ value: 'APPROVED', label: 'Approved' },
|
||||
{ value: 'REJECTED', label: 'Rejected' }
|
||||
];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.service
|
||||
.getPendingRequests(this.filterStatus, 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();
|
||||
}
|
||||
|
||||
review(req: any): void {
|
||||
this.reviewRequestId = req.id;
|
||||
this.showReviewModal = true;
|
||||
}
|
||||
|
||||
closeReviewModal(): void {
|
||||
this.showReviewModal = false;
|
||||
this.reviewRequestId = null;
|
||||
}
|
||||
|
||||
onReviewActionCompleted(): void {
|
||||
this.closeReviewModal();
|
||||
this.load();
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
PENDING: 'inv-badge--active',
|
||||
APPROVED: 'inv-badge--matured',
|
||||
REJECTED: 'inv-badge--written_off'
|
||||
};
|
||||
return `inv-badge ${map[status] ?? ''}`;
|
||||
}
|
||||
|
||||
statusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
PENDING: 'Pending',
|
||||
APPROVED: 'Approved',
|
||||
REJECTED: 'Rejected'
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,18 @@
|
||||
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
|
||||
<span>Share Withdrawal Requests</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./share-closure-requests']"
|
||||
routerLinkActive
|
||||
#shareClosureRequests="routerLinkActive"
|
||||
[active]="shareClosureRequests.isActive"
|
||||
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||
>
|
||||
<fa-icon icon="lock" class="ac-tab__ic"></fa-icon>
|
||||
<span>Share Closure Requests</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CheckerInboxAndTasksComponent } from './checker-inbox-and-tasks/checker
|
||||
import { CheckerInboxComponent } from './checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component';
|
||||
import { WithdrawalRequestsComponent } from './checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.component';
|
||||
import { ShareWithdrawalRequestsComponent } from './checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component';
|
||||
import { ShareClosureRequestsComponent } from './checker-inbox-and-tasks-tabs/share-closure-requests/share-closure-requests.component';
|
||||
import { DepositRequestsComponent } from './checker-inbox-and-tasks-tabs/deposit-requests/deposit-requests.component';
|
||||
import { DisbursementRequestsComponent } from './checker-inbox-and-tasks-tabs/disbursement-requests/disbursement-requests.component';
|
||||
import { ClientApprovalComponent } from './checker-inbox-and-tasks-tabs/client-approval/client-approval.component';
|
||||
@@ -165,6 +166,11 @@ const routes: Routes = [
|
||||
component: ShareWithdrawalRequestsComponent,
|
||||
data: { title: 'Share Withdrawal Requests' }
|
||||
},
|
||||
{
|
||||
path: 'share-closure-requests',
|
||||
component: ShareClosureRequestsComponent,
|
||||
data: { title: 'Share Closure Requests' }
|
||||
},
|
||||
{
|
||||
path: 'deposit-requests',
|
||||
component: DepositRequestsComponent,
|
||||
@@ -219,6 +225,19 @@ const routes: Routes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'checker-inbox-and-tasks/share-closure-requests',
|
||||
children: [
|
||||
{
|
||||
path: ':id/review',
|
||||
loadComponent: () =>
|
||||
import('../clerk/share-closure-request/share-closure-request.component').then(
|
||||
(m) => m.ShareClosureRequestComponent
|
||||
),
|
||||
data: { title: 'Review Share Closure Request', breadcrumb: 'Review Share Closure', mode: 'review' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'checker-inbox-and-tasks/disbursement-requests',
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user