feat(shares,tasks): centralized Additional Shares approval queue, remove unused Share Savings Debit
- Add Approval Center tab "Share Addition Requests": lists every pending additional-share tranche across all accounts (backed by the new /v1/shareadditionrequests endpoint), with a detail popup showing the board resolution and the route-specific proof of funding (voucher(s) for cash, or the linked savings account for savings). Approve/Reject call the same native approveadditionalshares/rejectadditionalshares commands the account-page buttons already used. - Remove "Debit from Savings" dialog/button and the "Share Savings Debit" Checker Inbox tab -- zero rows ever recorded on either tenant since it was built. - Remove "Approve Additional Shares"/"Reject Additional Shares" from the account-page menu now that every tranche is reviewed from the centralized queue instead. - Harden SharesButtonsConfiguration.removeButton(): a not-found name fed to splice(-1, 1) was quietly deleting the LAST button in the array instead of doing nothing -- removing the two menu entries above would otherwise have silently broken the rest of the Active-account button bar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -424,45 +424,16 @@ export class CashierService {
|
|||||||
return this.http.post(`/shareadvancecollection/${collectionId}/apply`, data);
|
return this.http.post(`/shareadvancecollection/${collectionId}/apply`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Every additional-share tranche awaiting approval, across every share account. */
|
||||||
* Whether this share account can be settled from the member's linked savings, and for how much.
|
getShareAdditionRequests(): Observable<any> {
|
||||||
*
|
return this.http.get('/shareadditionrequests');
|
||||||
* Reports a shortfall before the clerk raises anything, so the member can be told at the counter
|
|
||||||
* rather than after a request has been submitted and has to be rejected.
|
|
||||||
* @returns {Observable<any>} `{ eligible, reason, shareAmount, chargeAmount, totalRequired,
|
|
||||||
* savingsAccountNo, savingsBalance }`.
|
|
||||||
*/
|
|
||||||
getShareSavingsDebitEligibility(shareAccountId: number): Observable<any> {
|
|
||||||
return this.http.get(`/sharesavingsdebit/eligibility/${shareAccountId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a request to settle a share application by debiting the member's savings. Nothing moves
|
* Full detail for one pending tranche — the list row plus route-specific proof of funding: the
|
||||||
* until a manager approves it — unlike the cash route, where the cashier's receipt is already the
|
* voucher(s) behind a cash tranche, or the savings account already debited for a savings tranche.
|
||||||
* second pair of eyes.
|
|
||||||
*/
|
*/
|
||||||
submitShareSavingsDebit(data: any): Observable<any> {
|
getShareAdditionRequest(transactionId: number): Observable<any> {
|
||||||
return this.http.post('/sharesavingsdebit', data);
|
return this.http.get(`/shareadditionrequests/${transactionId}`);
|
||||||
}
|
|
||||||
|
|
||||||
/** Savings-debit requests, PENDING by default. */
|
|
||||||
getShareSavingsDebitRequests(status?: string, shareAccountId?: number): Observable<any> {
|
|
||||||
const params: string[] = [];
|
|
||||||
if (status) {
|
|
||||||
params.push(`status=${status}`);
|
|
||||||
}
|
|
||||||
if (shareAccountId) {
|
|
||||||
params.push(`shareAccountId=${shareAccountId}`);
|
|
||||||
}
|
|
||||||
return this.http.get(`/sharesavingsdebit${params.length ? '?' + params.join('&') : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Debits the member's savings and issues the shares. Manager only, and not the submitting clerk. */
|
|
||||||
approveShareSavingsDebit(id: number): Observable<any> {
|
|
||||||
return this.http.post(`/sharesavingsdebit/${id}/approve`, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectShareSavingsDebit(id: number, rejectionReason?: string): Observable<any> {
|
|
||||||
return this.http.post(`/sharesavingsdebit/${id}/reject`, { rejectionReason });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
<!--
|
|
||||||
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/.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<h2 mat-dialog-title>Debit from Savings</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content class="dfs-content">
|
|
||||||
@if (loading) {
|
|
||||||
<div class="dfs-loading">
|
|
||||||
<mat-spinner diameter="28"></mat-spinner>
|
|
||||||
<span>Checking the member's savings account…</span>
|
|
||||||
</div>
|
|
||||||
} @else if (errorMsg && !eligibility) {
|
|
||||||
<div class="dfs-error">{{ errorMsg }}</div>
|
|
||||||
} @else if (eligibility) {
|
|
||||||
<!-- What the settlement costs, split the way it will be posted: share value to Share Capital,
|
|
||||||
entry fee to fee income. -->
|
|
||||||
<div class="dfs-breakdown">
|
|
||||||
<div class="dfs-row">
|
|
||||||
<span>Share value</span>
|
|
||||||
<span>{{ eligibility.shareAmount | number: '1.2-2' }}</span>
|
|
||||||
</div>
|
|
||||||
@if (eligibility.chargeAmount > 0) {
|
|
||||||
<div class="dfs-row">
|
|
||||||
<span>Entry fee</span>
|
|
||||||
<span>{{ eligibility.chargeAmount | number: '1.2-2' }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="dfs-row dfs-row--total">
|
|
||||||
<span>To debit from savings</span>
|
|
||||||
<span>{{ eligibility.totalRequired | number: '1.2-2' }}</span>
|
|
||||||
</div>
|
|
||||||
@if (eligibility.savingsAccountNo) {
|
|
||||||
<div class="dfs-row dfs-row--detail">
|
|
||||||
<span>Savings account {{ eligibility.savingsAccountNo }}</span>
|
|
||||||
<span>Balance {{ eligibility.savingsBalance | number: '1.2-2' }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!eligibility.eligible) {
|
|
||||||
<div class="dfs-error">{{ eligibility.reason }}</div>
|
|
||||||
} @else {
|
|
||||||
<label class="dfs-field">
|
|
||||||
<span>Note (optional)</span>
|
|
||||||
<input type="text" [(ngModel)]="note" maxlength="500" placeholder="Anything to record against this request" />
|
|
||||||
</label>
|
|
||||||
<!-- Says plainly that submitting does not move money, so the clerk does not tell the member
|
|
||||||
their shares are settled when a manager still has to approve it. -->
|
|
||||||
<p class="dfs-hint">
|
|
||||||
Submitting records a request for a manager to approve. The member's savings are debited only once it is
|
|
||||||
approved.
|
|
||||||
</p>
|
|
||||||
@if (errorMsg) {
|
|
||||||
<div class="dfs-error">{{ errorMsg }}</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
|
||||||
<button mat-stroked-button (click)="cancel()">Cancel</button>
|
|
||||||
<button
|
|
||||||
mat-raised-button
|
|
||||||
color="primary"
|
|
||||||
[disabled]="loading || submitting || !eligibility?.eligible"
|
|
||||||
(click)="submit()"
|
|
||||||
>
|
|
||||||
{{ submitting ? 'Submitting…' : 'Submit for Approval' }}
|
|
||||||
</button>
|
|
||||||
</mat-dialog-actions>
|
|
||||||
-98
@@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.dfs-content {
|
|
||||||
min-width: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dfs-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 0;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 13.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* What the member is being charged, laid out as label/amount pairs so the total reads down the
|
|
||||||
column the way the posting splits it. */
|
|
||||||
.dfs-breakdown {
|
|
||||||
margin: 8px 0 16px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dfs-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 3px 0;
|
|
||||||
font-size: 13.5px;
|
|
||||||
color: #334155;
|
|
||||||
|
|
||||||
> span:last-child {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--total {
|
|
||||||
margin-top: 6px;
|
|
||||||
padding-top: 8px;
|
|
||||||
border-top: 1px solid #cbd5e1;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--detail {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dfs-field {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 12.5px;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13.5px;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #2563eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dfs-hint {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dfs-error {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fdecea;
|
|
||||||
color: #b42318;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
-121
@@ -1,121 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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, inject } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { CashierService } from 'app/cashier/cashier.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settles a share application out of the member's own savings account.
|
|
||||||
*
|
|
||||||
* The cash route (Apply Collection) starts at the counter, with money the cashier has already
|
|
||||||
* received sitting in Share Sundry against a receipt. A member who banks with the society has no
|
|
||||||
* reason to withdraw cash and hand it straight back, so this debits their savings directly for the
|
|
||||||
* share value plus any entry fee.
|
|
||||||
*
|
|
||||||
* Nothing moves when this dialog is submitted: it records a request for a manager to approve. The
|
|
||||||
* cash route needs no checker because the cashier's receipt is already a second pair of eyes; here
|
|
||||||
* nobody has counted anything, so the approval is the first moment the member's balance changes.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'mifosx-debit-from-savings-dialog',
|
|
||||||
templateUrl: './debit-from-savings-dialog.component.html',
|
|
||||||
styleUrls: ['./debit-from-savings-dialog.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
MatDialogModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatIconModule,
|
|
||||||
MatProgressSpinnerModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class DebitFromSavingsDialogComponent implements OnInit {
|
|
||||||
private cashierService = inject(CashierService);
|
|
||||||
|
|
||||||
loading = true;
|
|
||||||
submitting = false;
|
|
||||||
errorMsg = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What the settlement would cost and whether the savings account can cover it. `eligible` false
|
|
||||||
* carries a `reason` naming the blocker — no linked savings, already settled, short balance.
|
|
||||||
*/
|
|
||||||
eligibility: {
|
|
||||||
eligible: boolean;
|
|
||||||
reason: string | null;
|
|
||||||
shareAmount: number;
|
|
||||||
chargeAmount: number;
|
|
||||||
totalRequired: number;
|
|
||||||
savingsAccountNo: string | null;
|
|
||||||
savingsBalance: number | null;
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
note = '';
|
|
||||||
|
|
||||||
public dialogRef = inject(MatDialogRef<DebitFromSavingsDialogComponent>);
|
|
||||||
public data = inject<{ shareAccountId: number; clientId: number; accountNo: string }>(MAT_DIALOG_DATA);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.cashierService
|
|
||||||
.getShareSavingsDebitEligibility(this.data.shareAccountId)
|
|
||||||
.pipe(catchError(() => of(null)))
|
|
||||||
.subscribe((res: any) => {
|
|
||||||
this.loading = false;
|
|
||||||
if (!res) {
|
|
||||||
this.errorMsg = 'Could not check whether this account can be settled from savings.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.eligibility = {
|
|
||||||
eligible: !!res.eligible,
|
|
||||||
reason: res.reason ?? null,
|
|
||||||
shareAmount: Number(res.shareAmount) || 0,
|
|
||||||
chargeAmount: Number(res.chargeAmount) || 0,
|
|
||||||
totalRequired: Number(res.totalRequired) || 0,
|
|
||||||
savingsAccountNo: res.savingsAccountNo ?? null,
|
|
||||||
savingsBalance: res.savingsBalance == null ? null : Number(res.savingsBalance)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(): void {
|
|
||||||
if (!this.eligibility?.eligible || this.submitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.submitting = true;
|
|
||||||
this.errorMsg = '';
|
|
||||||
this.cashierService
|
|
||||||
.submitShareSavingsDebit({
|
|
||||||
shareAccountId: this.data.shareAccountId,
|
|
||||||
note: this.note?.trim() || undefined
|
|
||||||
})
|
|
||||||
.subscribe({
|
|
||||||
next: (res: any) => {
|
|
||||||
this.submitting = false;
|
|
||||||
this.dialogRef.close(res);
|
|
||||||
},
|
|
||||||
error: (err: any) => {
|
|
||||||
this.submitting = false;
|
|
||||||
this.errorMsg =
|
|
||||||
err?.error?.defaultUserMessage || err?.error?.message || 'Could not submit the savings-debit request.';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(): void {
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
||||||
import { ShareCertificateDialogComponent } from './share-certificate-dialog/share-certificate-dialog.component';
|
import { ShareCertificateDialogComponent } from './share-certificate-dialog/share-certificate-dialog.component';
|
||||||
import { ApplyCollectionDialogComponent } from './apply-collection-dialog/apply-collection-dialog.component';
|
import { ApplyCollectionDialogComponent } from './apply-collection-dialog/apply-collection-dialog.component';
|
||||||
import { DebitFromSavingsDialogComponent } from './debit-from-savings-dialog/debit-from-savings-dialog.component';
|
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { SharesService } from '../shares.service';
|
import { SharesService } from '../shares.service';
|
||||||
@@ -144,20 +143,6 @@ export class SharesAccountViewComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (status === 'Active') {
|
if (status === 'Active') {
|
||||||
const purchasedShares: any[] = this.sharesAccountData.purchasedShares;
|
|
||||||
let sharesPendingForApproval = false;
|
|
||||||
purchasedShares.forEach((share: any) => {
|
|
||||||
if (
|
|
||||||
share.status.code === 'purchasedSharesStatusType.applied' &&
|
|
||||||
share.type.code === 'purchasedSharesType.purchased'
|
|
||||||
) {
|
|
||||||
sharesPendingForApproval = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!sharesPendingForApproval) {
|
|
||||||
this.buttonConfig.removeButton('Approve Additional Shares');
|
|
||||||
this.buttonConfig.removeButton('Reject Additional Shares');
|
|
||||||
}
|
|
||||||
// An Active account can carry "approved" shares nobody has actually paid for yet -- account
|
// An Active account can carry "approved" shares nobody has actually paid for yet -- account
|
||||||
// creation, approval and activation post no GL of their own; only applying a Share Advance
|
// creation, approval and activation post no GL of their own; only applying a Share Advance
|
||||||
// Collection does (see isSettled). A certificate for shares still unsettled would certify
|
// Collection does (see isSettled). A certificate for shares still unsettled would certify
|
||||||
@@ -194,9 +179,6 @@ export class SharesAccountViewComponent implements OnInit {
|
|||||||
case 'Apply Collection':
|
case 'Apply Collection':
|
||||||
this.applyCollection();
|
this.applyCollection();
|
||||||
break;
|
break;
|
||||||
case 'Debit from Savings':
|
|
||||||
this.debitFromSavings();
|
|
||||||
break;
|
|
||||||
case 'Share Certificate':
|
case 'Share Certificate':
|
||||||
this.viewShareCertificate();
|
this.viewShareCertificate();
|
||||||
break;
|
break;
|
||||||
@@ -247,29 +229,12 @@ export class SharesAccountViewComponent implements OnInit {
|
|||||||
* Neither the account's status nor the purchase transaction's status can stand in for this: an
|
* Neither the account's status nor the purchase transaction's status can stand in for this: an
|
||||||
* account goes Approved then Activated before anything is settled, and the purchase reads
|
* account goes Approved then Activated before anything is settled, and the purchase reads
|
||||||
* Approved from the moment the account is approved. Only `collectionApplied` marks the point at
|
* Approved from the moment the account is approved. Only `collectionApplied` marks the point at
|
||||||
* which the money was actually taken — by cash collection or by debiting savings.
|
* which the money was actually taken by cash collection.
|
||||||
*/
|
*/
|
||||||
get isSettled(): boolean {
|
get isSettled(): boolean {
|
||||||
return !!this.sharesAccountData?.collectionApplied;
|
return !!this.sharesAccountData?.collectionApplied;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Settles this account out of the member's linked savings instead of cash at the counter.
|
|
||||||
*
|
|
||||||
* Only records a request — a manager's approval is what debits the member and issues the shares —
|
|
||||||
* so unlike Apply Collection there is nothing to refresh here until that approval happens.
|
|
||||||
*/
|
|
||||||
private debitFromSavings() {
|
|
||||||
this.dialog.open(DebitFromSavingsDialogComponent, {
|
|
||||||
width: '560px',
|
|
||||||
data: {
|
|
||||||
shareAccountId: this.sharesAccountData.id,
|
|
||||||
clientId: this.sharesAccountData.clientId,
|
|
||||||
accountNo: this.sharesAccountData.accountNo
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes Shares Account.
|
* Deletes Shares Account.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,32 +43,17 @@ export class SharesButtonsConfiguration {
|
|||||||
icon: 'hand-holding-usd',
|
icon: 'hand-holding-usd',
|
||||||
taskPermissionName: 'CREATE_JOURNALENTRY'
|
taskPermissionName: 'CREATE_JOURNALENTRY'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// The same settlement for a member who banks here: debits their linked savings for the
|
|
||||||
// share value plus entry fee instead of taking cash at the counter. Raises a request
|
|
||||||
// rather than posting, because no cashier has counted anything to check it against.
|
|
||||||
name: 'Debit from Savings',
|
|
||||||
icon: 'money-check-alt',
|
|
||||||
taskPermissionName: 'CREATE_JOURNALENTRY'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Apply Additional Shares',
|
name: 'Apply Additional Shares',
|
||||||
icon: 'arrow-right',
|
icon: 'arrow-right',
|
||||||
taskPermissionName: 'APPLYADDITIONAL_SHAREACCOUNT'
|
taskPermissionName: 'APPLYADDITIONAL_SHAREACCOUNT'
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Approve Additional Shares',
|
|
||||||
icon: 'arrow-right',
|
|
||||||
taskPermissionName: 'APPROVEADDITIONAL_SHAREACCOUNT'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Reject Additional Shares',
|
|
||||||
icon: 'arrow-left',
|
|
||||||
taskPermissionName: 'REJECTADDITIONAL_SHAREACCOUNT'
|
|
||||||
}
|
}
|
||||||
// 'Redeem Shares' is intentionally not offered here — a share withdrawal must go
|
// 'Approve Additional Shares' / 'Reject Additional Shares' are intentionally not offered
|
||||||
// through the Clerk Dashboard maker-checker flow (Clerk Dashboard > My Share Requests),
|
// here — every pending tranche across every account is reviewed and actioned from the
|
||||||
// never a direct one-step action from this view.
|
// Approval Center's Share Addition Requests queue instead, so a checker never has to know
|
||||||
|
// which account to open. 'Redeem Shares' is likewise not offered here — a share withdrawal
|
||||||
|
// must go through the Clerk Dashboard maker-checker flow (Clerk Dashboard > My Share
|
||||||
|
// Requests), never a direct one-step action from this view.
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case 'Submitted and pending approval':
|
case 'Submitted and pending approval':
|
||||||
@@ -147,7 +132,11 @@ export class SharesButtonsConfiguration {
|
|||||||
removeButton(name: string) {
|
removeButton(name: string) {
|
||||||
const buttonNames = this.buttonsArray.map((entry) => entry.name);
|
const buttonNames = this.buttonsArray.map((entry) => entry.name);
|
||||||
const index = buttonNames.indexOf(name);
|
const index = buttonNames.indexOf(name);
|
||||||
this.buttonsArray.splice(index, 1);
|
// -1 (not found) fed straight to splice would delete the LAST button instead of doing nothing —
|
||||||
|
// splice treats a negative index as "from the end", not "no match".
|
||||||
|
if (index > -1) {
|
||||||
|
this.buttonsArray.splice(index, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOption(name: string) {
|
removeOption(name: string) {
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<h2 mat-dialog-title>Additional Share Request</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content class="sard-content">
|
||||||
|
@if (loading) {
|
||||||
|
<div class="sard-loading">
|
||||||
|
<mat-spinner diameter="28"></mat-spinner>
|
||||||
|
<span>Loading request…</span>
|
||||||
|
</div>
|
||||||
|
} @else if (!detail) {
|
||||||
|
<div class="sard-error">{{ errorMsg }}</div>
|
||||||
|
} @else {
|
||||||
|
<div class="sard-grid">
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Client</span>
|
||||||
|
<span>{{ detail.clientName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Share account</span>
|
||||||
|
<span class="mono">{{ detail.accountNo }} — {{ detail.productName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Office</span>
|
||||||
|
<span>{{ detail.officeName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Requested shares</span>
|
||||||
|
<span>{{ detail.totalShares }} @ {{ detail.unitPrice | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row sard-row--total">
|
||||||
|
<span>Amount</span>
|
||||||
|
<span>{{ detail.amount | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
@if (detail.chargeAmount > 0) {
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Entry fee</span>
|
||||||
|
<span>{{ detail.chargeAmount | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Board resolution</span>
|
||||||
|
<span>{{ detail.resolutionNumber || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Meeting date</span>
|
||||||
|
<span>{{ detail.meetingDate || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Requested on</span>
|
||||||
|
<span>{{ detail.transactionDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sard-funding">
|
||||||
|
<div class="sard-funding-label">How it was funded</div>
|
||||||
|
|
||||||
|
@if (detail.fundingSource === 'CASH') {
|
||||||
|
@if (voucherCount > 0) {
|
||||||
|
<table class="sard-voucher-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ta-left">Voucher / Receipt</th>
|
||||||
|
<th class="ta-left">Date</th>
|
||||||
|
<th class="ta-left">Collected by</th>
|
||||||
|
<th class="ta-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (v of detail.vouchers; track v.id) {
|
||||||
|
<tr>
|
||||||
|
<td class="ta-left mono">{{ v.voucherNo || v.receiptNo }}</td>
|
||||||
|
<td class="ta-left">{{ v.collectionDate }}</td>
|
||||||
|
<td class="ta-left">{{ v.collectedByName || '—' }}</td>
|
||||||
|
<td class="ta-right amount-cell">{{ v.amount | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
} @else {
|
||||||
|
<div class="sard-funding-empty">Cash-funded, but no receipt is linked to this request.</div>
|
||||||
|
}
|
||||||
|
} @else if (detail.fundingSource === 'SAVINGS') {
|
||||||
|
@if (detail.savingsAccount) {
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Savings account</span>
|
||||||
|
<span class="mono">{{ detail.savingsAccount.accountNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-row">
|
||||||
|
<span>Current balance</span>
|
||||||
|
<span>{{ detail.savingsAccount.balance | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sard-funding-hint">
|
||||||
|
This amount was already debited from savings when the tranche was submitted.
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="sard-funding-empty">Savings-funded, but no linked savings account could be found.</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<div class="sard-funding-empty">No funding route was recorded for this request.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (errorMsg) {
|
||||||
|
<div class="sard-error">{{ errorMsg }}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-button (click)="cancel()" [disabled]="actioning">Close</button>
|
||||||
|
@if (detail) {
|
||||||
|
<button mat-stroked-button color="warn" [disabled]="actioning" (click)="reject()">
|
||||||
|
{{ actioning ? 'Working…' : 'Reject' }}
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="primary" [disabled]="actioning" (click)="approve()">
|
||||||
|
{{ actioning ? 'Working…' : 'Approve' }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</mat-dialog-actions>
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$primary: #1a3a6b;
|
||||||
|
$border: #d8e0ec;
|
||||||
|
$muted: #6b7a90;
|
||||||
|
$danger: #b3261e;
|
||||||
|
|
||||||
|
.sard-content {
|
||||||
|
min-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid #f0f3f8;
|
||||||
|
|
||||||
|
> span:first-child {
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span:last-child {
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-row--total {
|
||||||
|
> span:last-child {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-funding {
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-funding-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
color: $muted;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-funding-empty,
|
||||||
|
.sard-funding-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-voucher-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: $muted;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 7px 8px;
|
||||||
|
border-top: 1px solid #eceff4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sard-error {
|
||||||
|
color: $danger;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
+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 { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { CashierService } from 'app/cashier/cashier.service';
|
||||||
|
import { SharesService } from 'app/shares/shares.service';
|
||||||
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full detail for one additional-share request, opened from the Approval Center list.
|
||||||
|
*
|
||||||
|
* Everything a checker needs to decide without leaving the queue: the board resolution, the tranche
|
||||||
|
* value, and — the piece a bare list row cannot show — the actual proof of funding. A cash tranche
|
||||||
|
* shows every receipt behind it (a member paying in instalments has more than one), a savings
|
||||||
|
* tranche shows the account already debited and its balance, so the approval is never a guess about
|
||||||
|
* where the money came from.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-share-addition-request-detail-dialog',
|
||||||
|
templateUrl: './share-addition-request-detail-dialog.component.html',
|
||||||
|
styleUrls: ['./share-addition-request-detail-dialog.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressSpinnerModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ShareAdditionRequestDetailDialogComponent implements OnInit {
|
||||||
|
private cashierService = inject(CashierService);
|
||||||
|
private sharesService = inject(SharesService);
|
||||||
|
private settingsService = inject(SettingsService);
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
errorMsg = '';
|
||||||
|
actioning = false;
|
||||||
|
detail: any = null;
|
||||||
|
|
||||||
|
public dialogRef = inject(MatDialogRef<ShareAdditionRequestDetailDialogComponent>);
|
||||||
|
public data = inject<{ transactionId: number }>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.cashierService
|
||||||
|
.getShareAdditionRequest(this.data.transactionId)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.errorMsg = this.readError(err, 'Could not load this request.');
|
||||||
|
this.loading = false;
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((res: any) => {
|
||||||
|
if (res) {
|
||||||
|
this.detail = res;
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many receipts fund this tranche — the lead one plus any further instalments. */
|
||||||
|
get voucherCount(): number {
|
||||||
|
return this.detail?.vouchers?.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
approve(): void {
|
||||||
|
this.act('approveadditionalshares');
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(): void {
|
||||||
|
this.act('rejectadditionalshares');
|
||||||
|
}
|
||||||
|
|
||||||
|
private act(command: string): void {
|
||||||
|
if (this.actioning || !this.detail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.actioning = true;
|
||||||
|
this.errorMsg = '';
|
||||||
|
const data = {
|
||||||
|
requestedShares: [{ id: this.detail.transactionId }],
|
||||||
|
dateFormat: this.settingsService.dateFormat,
|
||||||
|
locale: this.settingsService.language.code
|
||||||
|
};
|
||||||
|
this.sharesService
|
||||||
|
.executeSharesAccountCommand(this.detail.shareAccountId, command, data)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.errorMsg = this.readError(err, 'The request could not be actioned.');
|
||||||
|
this.actioning = false;
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((res: any) => {
|
||||||
|
if (res) {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
} else {
|
||||||
|
this.actioning = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readError(err: any, fallback: string): string {
|
||||||
|
return (
|
||||||
|
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||||
|
err?.error?.defaultUserMessage ||
|
||||||
|
err?.error?.developerMessage ||
|
||||||
|
fallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
<!--
|
||||||
|
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="sar-container">
|
||||||
|
<p class="sar-intro">
|
||||||
|
Additional-share tranches waiting for approval, across every share account. Click a row for the full request — the
|
||||||
|
board resolution, how it was funded, and the voucher or savings account behind it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (errorMsg) {
|
||||||
|
<div class="sar-error">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<span>{{ errorMsg }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (loading) {
|
||||||
|
<div class="sar-loading">
|
||||||
|
<mat-spinner diameter="28"></mat-spinner>
|
||||||
|
<span>Loading pending requests…</span>
|
||||||
|
</div>
|
||||||
|
} @else if (requests.length === 0) {
|
||||||
|
<div class="sar-empty">
|
||||||
|
<mat-icon>info_outline</mat-icon>
|
||||||
|
<span>No additional-share requests are waiting for approval.</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="sar-table-wrap">
|
||||||
|
<table class="sar-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ta-left">Client</th>
|
||||||
|
<th class="ta-left">Account</th>
|
||||||
|
<th class="ta-right">Shares</th>
|
||||||
|
<th class="ta-right">Amount</th>
|
||||||
|
<th class="ta-left">Funding</th>
|
||||||
|
<th class="ta-left">Resolution</th>
|
||||||
|
<th class="ta-left">Date</th>
|
||||||
|
<th class="ta-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (req of requests; track req.transactionId) {
|
||||||
|
<tr (click)="viewDetail(req)">
|
||||||
|
<td class="ta-left">{{ req.clientName }}</td>
|
||||||
|
<td class="ta-left mono">{{ req.accountNo }}</td>
|
||||||
|
<td class="ta-right">{{ req.totalShares }}</td>
|
||||||
|
<td class="ta-right amount-cell">{{ req.amount | number: '1.2-2' }}</td>
|
||||||
|
<td class="ta-left">{{ fundingLabel(req) }}</td>
|
||||||
|
<td class="ta-left">{{ req.resolutionNumber || '—' }}</td>
|
||||||
|
<td class="ta-left">{{ req.transactionDate }}</td>
|
||||||
|
<td class="ta-center sar-actions" (click)="$event.stopPropagation()">
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="actioningId === req.transactionId"
|
||||||
|
(click)="approve(req)"
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
color="warn"
|
||||||
|
type="button"
|
||||||
|
[disabled]="actioningId === req.transactionId"
|
||||||
|
(click)="reject(req)"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$primary: #1a3a6b;
|
||||||
|
$border: #d8e0ec;
|
||||||
|
$muted: #6b7a90;
|
||||||
|
$danger: #b3261e;
|
||||||
|
|
||||||
|
.sar-container {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sar-intro {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $muted;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sar-loading,
|
||||||
|
.sar-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $muted;
|
||||||
|
background: #f4f7fb;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sar-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #fdecea;
|
||||||
|
color: $danger;
|
||||||
|
border: 1px solid #f5c6c2;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sar-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sar-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #eef2f8;
|
||||||
|
color: $primary;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #eceff4;
|
||||||
|
color: #1f2937;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f4f7fb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 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 { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { CashierService } from 'app/cashier/cashier.service';
|
||||||
|
import { SharesService } from 'app/shares/shares.service';
|
||||||
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
import { ShareAdditionRequestDetailDialogComponent } from './share-addition-request-detail-dialog/share-addition-request-detail-dialog.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approval Center — Share Addition Requests tab.
|
||||||
|
*
|
||||||
|
* Every additional-share tranche across every member, waiting on the same
|
||||||
|
* {@code approveadditionalshares}/{@code rejectadditionalshares} commands that used to be reachable only from a
|
||||||
|
* single account's own page. A checker working through pending items no longer has to know which account to open —
|
||||||
|
* every request queued across the whole membership is one list here, and clicking a row opens the full detail (the
|
||||||
|
* board resolution, how it was funded, and — for cash — which voucher, or — for savings — which account was already
|
||||||
|
* debited) before deciding.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-share-addition-requests',
|
||||||
|
templateUrl: './share-addition-requests.component.html',
|
||||||
|
styleUrls: ['./share-addition-requests.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressSpinnerModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ShareAdditionRequestsComponent implements OnInit {
|
||||||
|
private cashierService = inject(CashierService);
|
||||||
|
private sharesService = inject(SharesService);
|
||||||
|
private settingsService = inject(SettingsService);
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
errorMsg = '';
|
||||||
|
requests: any[] = [];
|
||||||
|
actioningId: number | null = null;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.errorMsg = '';
|
||||||
|
this.cashierService
|
||||||
|
.getShareAdditionRequests()
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.errorMsg = this.readError(err, 'Could not load pending additional-share requests.');
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((res: any) => {
|
||||||
|
this.requests = Array.isArray(res) ? res : [];
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many receipts fund a cash tranche — the lead one plus any further instalments. */
|
||||||
|
voucherCount(req: any): number {
|
||||||
|
if (!req?.advanceCollectionId) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const extra = (req.additionalCollectionIds || '')
|
||||||
|
.split(',')
|
||||||
|
.map((part: string) => part.trim())
|
||||||
|
.filter((part: string) => part.length > 0);
|
||||||
|
return 1 + extra.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingLabel(req: any): string {
|
||||||
|
switch (req?.fundingSource) {
|
||||||
|
case 'CASH': {
|
||||||
|
const count = this.voucherCount(req);
|
||||||
|
return count > 1 ? `Cash (${count} vouchers)` : 'Cash';
|
||||||
|
}
|
||||||
|
case 'SAVINGS':
|
||||||
|
return 'Savings';
|
||||||
|
default:
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the full-detail popup for one request. Approving or rejecting from inside it refreshes
|
||||||
|
* this list on close, the same way the account-page tables refresh after acting on a row.
|
||||||
|
*/
|
||||||
|
viewDetail(req: any): void {
|
||||||
|
const dialogRef = this.dialog.open(ShareAdditionRequestDetailDialogComponent, {
|
||||||
|
width: '640px',
|
||||||
|
data: { transactionId: req.transactionId }
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe((actioned: boolean) => {
|
||||||
|
if (actioned) {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Approves directly from the list row, for a request the checker does not need the detail popup for. */
|
||||||
|
approve(req: any): void {
|
||||||
|
if (this.actioningId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.actioningId = req.transactionId;
|
||||||
|
this.runCommand(req, 'approveadditionalshares');
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(req: any): void {
|
||||||
|
if (this.actioningId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.actioningId = req.transactionId;
|
||||||
|
this.runCommand(req, 'rejectadditionalshares');
|
||||||
|
}
|
||||||
|
|
||||||
|
private runCommand(req: any, command: string): void {
|
||||||
|
const data = {
|
||||||
|
requestedShares: [{ id: req.transactionId }],
|
||||||
|
dateFormat: this.settingsService.dateFormat,
|
||||||
|
locale: this.settingsService.language.code
|
||||||
|
};
|
||||||
|
this.sharesService
|
||||||
|
.executeSharesAccountCommand(req.shareAccountId, command, data)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.errorMsg = this.readError(err, 'The request could not be actioned.');
|
||||||
|
this.actioningId = null;
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((res: any) => {
|
||||||
|
this.actioningId = null;
|
||||||
|
if (res) {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readError(err: any, fallback: string): string {
|
||||||
|
return (
|
||||||
|
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||||
|
err?.error?.defaultUserMessage ||
|
||||||
|
err?.error?.developerMessage ||
|
||||||
|
fallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
<!--
|
|
||||||
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 Savings Debit — 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>Member</th>
|
|
||||||
<th>Share Account</th>
|
|
||||||
<th>Savings Account</th>
|
|
||||||
<th class="num">Shares</th>
|
|
||||||
<th class="num">Entry Fee</th>
|
|
||||||
<th class="num">To Debit</th>
|
|
||||||
<th>Requested By</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@if (loading) {
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" class="pg-empty">Loading…</td>
|
|
||||||
</tr>
|
|
||||||
} @else if (loaded && rows.length === 0) {
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" class="pg-empty">No savings-debit requests are waiting for approval.</td>
|
|
||||||
</tr>
|
|
||||||
} @else {
|
|
||||||
@for (row of rows; track row.id) {
|
|
||||||
<tr>
|
|
||||||
<td>{{ row.client_name }}</td>
|
|
||||||
<td>{{ row.share_account_no }}</td>
|
|
||||||
<td>
|
|
||||||
{{ row.savings_account_no }}
|
|
||||||
<!-- The balance can move between the clerk raising this and a manager reaching it,
|
|
||||||
and the server refuses a short account — better seen before clicking. -->
|
|
||||||
<div [class.ssd-warn]="isShort(row)" class="ssd-sub">
|
|
||||||
Balance {{ row.savings_balance | number: '1.2-2' }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="num">{{ row.share_amount | number: '1.2-2' }}</td>
|
|
||||||
<td class="num">{{ row.charge_amount | number: '1.2-2' }}</td>
|
|
||||||
<td class="num">
|
|
||||||
<strong>{{ row.total_amount | number: '1.2-2' }}</strong>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ row.clerk_name }}
|
|
||||||
<div class="ssd-sub">{{ row.submitted_at | slice: 0 : 10 }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="ssd-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pg-btn"
|
|
||||||
[disabled]="actioningId === row.id || isShort(row)"
|
|
||||||
(click)="approve(row)"
|
|
||||||
>
|
|
||||||
{{ actioningId === row.id ? 'Working…' : 'Approve' }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="pg-btn" [disabled]="actioningId === row.id" (click)="toggleReject(row)">
|
|
||||||
{{ expandedRejectId === row.id ? 'Cancel' : 'Reject' }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@if (isShort(row)) {
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" class="ssd-warn-row">
|
|
||||||
Savings holds {{ row.savings_balance | number: '1.2-2' }} but
|
|
||||||
{{ row.total_amount | number: '1.2-2' }} is required. Take a deposit first, or reject this and settle by
|
|
||||||
cash collection.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (actionError[row.id]) {
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" class="ssd-warn-row">{{ actionError[row.id] }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (expandedRejectId === row.id) {
|
|
||||||
<tr>
|
|
||||||
<td colspan="8">
|
|
||||||
<div class="inv-form-grid" style="padding: 8px 0">
|
|
||||||
<label class="inv-form-field">
|
|
||||||
<span>Reason for rejection</span>
|
|
||||||
<input type="text" [(ngModel)]="rejectReason[row.id]" maxlength="500" />
|
|
||||||
</label>
|
|
||||||
<label class="inv-form-field" style="align-self: end">
|
|
||||||
<button type="button" class="pg-btn" [disabled]="actioningId === row.id" (click)="reject(row)">
|
|
||||||
Confirm Rejection
|
|
||||||
</button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* A savings account that no longer covers the request, and any error the server returned. Both
|
|
||||||
read as blocking conditions on the row, so they share the same treatment. */
|
|
||||||
.ssd-warn {
|
|
||||||
color: #b42318;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssd-warn-row {
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #fdecea;
|
|
||||||
color: #b42318;
|
|
||||||
font-size: 12.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssd-sub {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssd-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
-120
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -166,12 +166,12 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="ac-tab"
|
class="ac-tab"
|
||||||
routerLink="./share-savings-debit-requests"
|
routerLink="./share-addition-requests"
|
||||||
routerLinkActive="ac-tab--active"
|
routerLinkActive="ac-tab--active"
|
||||||
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||||
>
|
>
|
||||||
<fa-icon icon="money-check-alt" class="ac-tab__ic"></fa-icon>
|
<fa-icon icon="money-check-alt" class="ac-tab__ic"></fa-icon>
|
||||||
<span>Share Savings Debit</span>
|
<span>Share Addition Requests</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="ac-tab"
|
class="ac-tab"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { CheckerInboxComponent } from './checker-inbox-and-tasks-tabs/checker-in
|
|||||||
import { WithdrawalRequestsComponent } from './checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.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 { 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 { DividendRequestsComponent } from './checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component';
|
||||||
import { ShareSavingsDebitRequestsComponent } from './checker-inbox-and-tasks-tabs/share-savings-debit-requests/share-savings-debit-requests.component';
|
import { ShareAdditionRequestsComponent } from './checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-requests.component';
|
||||||
import { DepositRequestsComponent } from './checker-inbox-and-tasks-tabs/deposit-requests/deposit-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 { 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';
|
import { ClientApprovalComponent } from './checker-inbox-and-tasks-tabs/client-approval/client-approval.component';
|
||||||
@@ -183,9 +183,9 @@ const routes: Routes = [
|
|||||||
data: { title: 'Dividend Requests' }
|
data: { title: 'Dividend Requests' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'share-savings-debit-requests',
|
path: 'share-addition-requests',
|
||||||
component: ShareSavingsDebitRequestsComponent,
|
component: ShareAdditionRequestsComponent,
|
||||||
data: { title: 'Share Savings Debit Requests' }
|
data: { title: 'Share Addition Requests' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'deposit-requests',
|
path: 'deposit-requests',
|
||||||
|
|||||||
Reference in New Issue
Block a user