feat(shares): real-profit driven dividend UI, Dividend Dashboard, and member claim workflow
Frontend counterpart to the backend dividend rework: - Create Dividend screen auto-computes the amount from real bank profit x officer percentage, blocks overlapping declaration periods, and adds a Total Dividend Paid summary card. - View Dividend page modernized and reusable as an embedded popup from the new cross-product "Dividend Requests" approval-center tab. - New consolidated Dividend Dashboard: summary stats, every declaration and its status, locked-dividend-vs-overdue-EMI matching with a proactive apply-to-loan action, full 3-year lock history, and a maker-checker claim panel letting a clerk split a member's unclaimed dividend across an overdue loan, savings credit, or cash for a manager to approve. - Menu/routing consolidated under Share > Dividend Management; removed the now-unreachable Failed Dividend Payouts page and its home-dashboard alert once the backend stopped needing a retry path for stuck payouts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
||||
<!--
|
||||
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">Dividend Requests — Declared, Awaiting Approval</div>
|
||||
<div class="inv-form-grid" style="padding: 12px 16px">
|
||||
<label class="inv-form-field" style="align-self: end">
|
||||
<button type="button" class="pg-btn" [disabled]="loading" (click)="load()">Refresh</button>
|
||||
</label>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Share Product</th>
|
||||
<th>Dividend Period</th>
|
||||
<th class="num">Total Amount</th>
|
||||
<th class="num">Members</th>
|
||||
<th>Declared On</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading) {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">Loading…</td>
|
||||
</tr>
|
||||
} @else if (loaded && rows.length === 0) {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No dividend declarations are waiting for approval.</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (row of rows; track row.dividendId) {
|
||||
<tr>
|
||||
<td>{{ row.productName }}</td>
|
||||
<td>{{ row.dividendPeriodStartDate }} → {{ row.dividendPeriodEndDate }}</td>
|
||||
<td class="num">{{ row.amount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ row.memberCount }}</td>
|
||||
<td>{{ row.declaredOn | slice: 0 : 10 }}</td>
|
||||
<td class="dr-actions">
|
||||
<button type="button" class="pg-btn" (click)="openView(row)">View</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn dr-approve-btn"
|
||||
[disabled]="approvingId === row.dividendId"
|
||||
(click)="approve(row)"
|
||||
>
|
||||
{{ approvingId === row.dividendId ? 'Approving…' : 'Approve' }}
|
||||
</button>
|
||||
@if (approveError[row.dividendId]) {
|
||||
<div class="dr-approve-error">{{ approveError[row.dividendId] }}</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (showViewModal && viewProductId && viewDividendId) {
|
||||
<div class="dr-popup-backdrop" (click)="closeViewModal()">
|
||||
<div class="dr-popup-panel" (click)="$event.stopPropagation()">
|
||||
<mifosx-view-dividend
|
||||
[embedded]="true"
|
||||
[overrideProductId]="viewProductId"
|
||||
[overrideDividendId]="viewDividendId"
|
||||
(closeRequested)="closeViewModal()"
|
||||
(actionCompleted)="onViewActionCompleted()"
|
||||
></mifosx-view-dividend>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.dr-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dr-approve-btn {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border-color: #2563eb;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.dr-approve-error {
|
||||
width: 100%;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@keyframes dr-popup-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dr-popup-zoom {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dr-popup-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
// No backdrop-filter here: it would make this element a containing block for any
|
||||
// position:fixed descendant inside the embedded ViewDividendComponent.
|
||||
background: rgb(15 25 45 / 65%);
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 32px 20px;
|
||||
overflow-y: auto;
|
||||
animation: dr-popup-fade 0.2s ease-out;
|
||||
}
|
||||
|
||||
.dr-popup-panel {
|
||||
background: #f7f9fc;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
box-shadow: 0 24px 60px rgb(0 0 0 / 25%);
|
||||
animation: dr-popup-zoom 0.25s ease-out;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dr-popup-backdrop,
|
||||
.dr-popup-panel {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { catchError } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { PendingDividendsService } from './pending-dividends.service';
|
||||
import { ProductsService } from '../../../products/products.service';
|
||||
import { ViewDividendComponent } from '../../../products/share-products/view-dividend/view-dividend.component';
|
||||
|
||||
/**
|
||||
* Approval Center tab — every Share Product dividend declaration still awaiting approval, across
|
||||
* all products (Fineract's own screens only show one product's dividends at a time). Approving
|
||||
* directly from here calls the same native Fineract "approve dividend" command used by the
|
||||
* per-product View Dividend page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-dividend-requests',
|
||||
templateUrl: './dividend-requests.component.html',
|
||||
styleUrls: [
|
||||
'../../../cashier/cashier-shared.scss',
|
||||
'../../../clerk/clerk-shared.scss',
|
||||
'./dividend-requests.component.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ViewDividendComponent
|
||||
]
|
||||
})
|
||||
export class DividendRequestsComponent implements OnInit {
|
||||
private service = inject(PendingDividendsService);
|
||||
private productsService = inject(ProductsService);
|
||||
|
||||
rows: any[] = [];
|
||||
loading = false;
|
||||
loaded = false;
|
||||
|
||||
approvingId: number | null = null;
|
||||
approveError: Record<number, string> = {};
|
||||
|
||||
showViewModal = false;
|
||||
viewProductId: number | null = null;
|
||||
viewDividendId: number | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
openView(row: any): void {
|
||||
this.viewProductId = row.productId;
|
||||
this.viewDividendId = row.dividendId;
|
||||
this.showViewModal = true;
|
||||
}
|
||||
|
||||
closeViewModal(): void {
|
||||
this.showViewModal = false;
|
||||
this.viewProductId = null;
|
||||
this.viewDividendId = null;
|
||||
}
|
||||
|
||||
onViewActionCompleted(): void {
|
||||
this.closeViewModal();
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.service
|
||||
.list()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((res: any[]) => {
|
||||
this.rows = res ?? [];
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
approve(row: any): void {
|
||||
this.approvingId = row.dividendId;
|
||||
delete this.approveError[row.dividendId];
|
||||
this.productsService
|
||||
.approveDividend(row.productId, row.dividendId, { productId: row.productId, dividendId: row.dividendId })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.approvingId = null;
|
||||
this.load();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.approvingId = null;
|
||||
this.approveError[row.dividendId] =
|
||||
err?.error?.defaultUserMessage || err?.error?.message || 'Approve failed.';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* HTTP service listing every Share Product dividend declaration still awaiting approval, across
|
||||
* all products — Fineract's own dividend endpoints are scoped to one product at a time, so this
|
||||
* is what feeds the "Dividend Requests" tab on the Approval Center (Checker Inbox).
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PendingDividendsService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
list(): Observable<any> {
|
||||
return this.http.get('/clerk/pending-dividends');
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,15 @@
|
||||
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
|
||||
<span>Share Withdrawal Requests</span>
|
||||
</a>
|
||||
<a
|
||||
class="ac-tab"
|
||||
routerLink="./dividend-requests"
|
||||
routerLinkActive="ac-tab--active"
|
||||
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||
>
|
||||
<fa-icon icon="coins" class="ac-tab__ic"></fa-icon>
|
||||
<span>Dividend Requests</span>
|
||||
</a>
|
||||
<a
|
||||
class="ac-tab"
|
||||
routerLink="./deposit-requests"
|
||||
|
||||
@@ -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 { DividendRequestsComponent } from './checker-inbox-and-tasks-tabs/dividend-requests/dividend-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: 'dividend-requests',
|
||||
component: DividendRequestsComponent,
|
||||
data: { title: 'Dividend Requests' }
|
||||
},
|
||||
{
|
||||
path: 'deposit-requests',
|
||||
component: DepositRequestsComponent,
|
||||
|
||||
Reference in New Issue
Block a user