share certificate and share product maker checker

This commit is contained in:
2026-07-27 13:57:49 +05:30
parent d0536dbb11
commit 0c99dc3686
47 changed files with 2790 additions and 245 deletions
@@ -0,0 +1,108 @@
<!--
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="psae-tab-content">
@if (showData) {
<div class="psae-action-bar">
<mat-form-field appearance="outline" class="psae-search-field">
<mat-icon matPrefix>search</mat-icon>
<input #filterInput matInput placeholder="Search by account number" (keyup)="applyFilter(filterInput.value)" />
</mat-form-field>
</div>
<div class="table-container">
<table mat-table [dataSource]="dataSource" class="modern-table">
<ng-container matColumnDef="accountNo">
<th mat-header-cell *matHeaderCellDef>Account No.</th>
<td mat-cell *matCellDef="let edit">{{ edit.accountNo }}</td>
</ng-container>
<ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef>Client</th>
<td mat-cell *matCellDef="let edit">{{ edit.clientName }}</td>
</ng-container>
<ng-container matColumnDef="makerUsername">
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
<td mat-cell *matCellDef="let edit">{{ edit.makerUsername }}</td>
</ng-container>
<ng-container matColumnDef="createdOn">
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
<td mat-cell *matCellDef="let edit">{{ formatDate(edit.createdOn) }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let edit">
<span class="status-badge status-badge--pending">{{ edit.status }}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let edit">
<button mat-stroked-button class="psae-btn psae-btn-view" (click)="viewCompare(edit)">
<fa-icon icon="eye" class="m-r-10"></fa-icon>Compare
</button>
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
</button>
<button mat-stroked-button class="psae-btn psae-btn-reject" (click)="reject(edit)">
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
</table>
</div>
@if (selectedEdit) {
<div class="psae-diff-panel">
<div class="psae-diff-header">
<h3>Proposed Changes &mdash; Account "{{ selectedEdit.accountNo }}"</h3>
<button mat-icon-button (click)="closeCompare()"><fa-icon icon="times"></fa-icon></button>
</div>
<div class="psae-diff-block">
<h4>Details</h4>
<table class="psae-diff-table">
<thead>
<tr>
<th>Field</th>
<th>Proposed Value</th>
</tr>
</thead>
<tbody>
@for (row of entries(selectedEdit); track row.key) {
<tr>
<td class="psae-diff-label">{{ row.key }}</td>
<td class="psae-diff-proposed">{{ row.value }}</td>
</tr>
}
</tbody>
</table>
</div>
@if (selectedEdit.rejectReason) {
<p class="psae-reject-reason"><strong>Reject Reason:</strong> {{ selectedEdit.rejectReason }}</p>
}
</div>
}
}
@if (!showData) {
<div class="empty-state">
<fa-icon icon="hourglass-half" class="empty-icon"></fa-icon>
<p class="empty-title">No pending account edits</p>
<p class="empty-text">There are no Share Account details edits awaiting approval at this time.</p>
</div>
}
</div>
@@ -0,0 +1,190 @@
/**
* 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/.
*/
$border: #e2e8f0;
$text-muted: #64748b;
.psae-tab-content {
padding: 16px 0;
}
.psae-action-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.psae-search-field {
flex: 1;
min-width: 200px;
max-width: 320px;
}
.psae-btn {
font-size: 11.5px;
font-weight: 600;
border-radius: 8px !important;
height: 32px;
padding: 0 10px;
margin-right: 6px;
fa-icon {
margin-right: 4px;
font-size: 11px;
}
}
.psae-btn-view {
border-color: #2563eb;
color: #2563eb;
&:hover {
background: #eff6ff;
}
}
.psae-btn-approve {
border-color: #16a34a;
color: #16a34a;
&:hover {
background: #f0fdf4;
}
}
.psae-btn-reject {
border-color: #dc2626;
color: #dc2626;
&:hover {
background: #fef2f2;
}
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
&--pending {
background: #fef9c3;
color: #854d0e;
}
}
.psae-diff-panel {
margin-top: 20px;
border: 1px solid $border;
border-radius: 12px;
background: #fff;
padding: 16px;
}
.psae-diff-header {
display: flex;
align-items: center;
justify-content: space-between;
h3 {
margin: 0;
font-size: 14px;
font-weight: 700;
}
}
.psae-diff-block {
margin-top: 16px;
h4 {
margin: 0 0 6px;
font-size: 12.5px;
font-weight: 700;
color: #0d1b2a;
text-transform: uppercase;
letter-spacing: 0.03em;
}
}
.psae-diff-loading {
color: $text-muted;
font-size: 13px;
}
.psae-diff-table {
width: 100%;
border-collapse: collapse;
margin-top: 4px;
font-size: 12.5px;
th,
td {
text-align: left;
padding: 8px 10px;
border-bottom: 1px solid $border;
}
th {
color: $text-muted;
font-weight: 600;
}
}
.psae-diff-label {
font-weight: 600;
color: #0d1b2a;
}
.psae-diff-proposed {
color: #15803d;
font-weight: 600;
}
.psae-reject-reason {
margin-top: 14px;
font-size: 12.5px;
color: #b91c1c;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 24px;
background: #fff;
border: 1px dashed $border;
border-radius: 14px;
text-align: center;
margin: 16px 0;
.empty-icon {
font-size: 42px;
color: #cbd5e1;
margin-bottom: 14px;
}
.empty-title {
margin: 0 0 4px;
font-size: 15px;
font-weight: 700;
color: #0d1b2a;
}
.empty-text {
margin: 0;
font-size: 12.5px;
color: $text-muted;
max-width: 380px;
line-height: 1.5;
}
}
@@ -0,0 +1,192 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { SharesService } from 'app/shares/shares.service';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Checker-facing list of Share Account details edits awaiting approval.
* Mirrors PendingSavingsAccountEditsComponent's list + compare + approve/reject pattern.
*/
@Component({
selector: 'mifosx-pending-share-account-edits',
templateUrl: './pending-share-account-edits.component.html',
styleUrls: ['./pending-share-account-edits.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatIcon,
MatIconButton
]
})
export class PendingShareAccountEditsComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private dialog = inject(MatDialog);
private sharesService = inject(SharesService);
dataSource: MatTableDataSource<any>;
showData = false;
displayedColumns: string[] = [
'accountNo',
'clientName',
'makerUsername',
'createdOn',
'status',
'actions'
];
selectedEdit: any = null;
constructor() {
this.route.data.subscribe((data: { pendingShareAccountEditsData: any }) => {
const items = data.pendingShareAccountEditsData || [];
this.showData = items.length > 0;
this.dataSource = new MatTableDataSource(items);
});
}
applyFilter(filterValue: string = '') {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
formatDate(date: any): string {
if (!date) return '';
const d = Array.isArray(date)
? new Date(date[0], date[1] - 1, date[2], date[3] || 0, date[4] || 0)
: new Date(date);
return isNaN(d.getTime()) ? String(date) : d.toLocaleString('en-IN');
}
/** Readable list of "key: value" pairs for the proposed details changes. */
entries(edit: any): { key: string; value: any }[] {
if (!edit) return [];
const fields: [string, any][] = [
[
'Linked Savings Account',
edit.savingsAccountNo
],
[
'Allow Dividends for Inactive Clients',
edit.allowDividendCalculationForInactiveClients
],
[
'Lockin Period Frequency',
edit.lockinPeriodFrequency
],
[
'Minimum Active Period Frequency',
edit.minimumActivePeriodFrequency
]
];
return fields
.filter(
([
,
value
]) => value !== null && value !== undefined
)
.map(
([
key,
value
]) => ({ key, value })
);
}
viewCompare(edit: any): void {
this.selectedEdit = this.selectedEdit === edit ? null : edit;
}
closeCompare(): void {
this.selectedEdit = null;
}
approve(edit: any): void {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
heading: 'Approve Share Account Edit',
dialogContext: `Apply the pending details edit for account "${edit.accountNo}"? This cannot be undone.`,
type: 'basic'
}
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response?.confirm) {
this.sharesService.approveShareAccountEdit(edit.shareAccountId).subscribe(() => this.reload());
}
});
}
reject(edit: any): void {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'rejectReason',
label: 'Reason for rejection',
value: '',
type: 'text',
required: false
})
];
const dialogRef = this.dialog.open(FormDialogComponent, {
data: {
title: `Reject edit for account "${edit.accountNo}"`,
layout: { addButtonText: 'Reject' },
formfields
}
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response?.data) {
const rejectReason = response.data.value.rejectReason || '';
this.sharesService.rejectShareAccountEdit(edit.shareAccountId, rejectReason).subscribe(() => this.reload());
}
});
}
reload(): void {
this.closeCompare();
const url = this.router.url;
this.router
.navigateByUrl('/checker-inbox-and-tasks', { skipLocationChange: true })
.then(() => this.router.navigate([url]));
}
}
@@ -118,6 +118,18 @@
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Account Edits</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./pending-share-account-edits']"
routerLinkActive
#pendingShareAccountEdits="routerLinkActive"
[active]="pendingShareAccountEdits.isActive"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Share Account Edits</span>
</a>
<a
mat-tab-link
class="ac-tab"
@@ -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 { Resolve } from '@angular/router';
import { Observable } from 'rxjs';
import { SharesService } from 'app/shares/shares.service';
/**
* Resolves all Share Account details edits currently awaiting checker approval, for the Pending Share
* Account Edits screen.
*/
@Injectable()
export class GetPendingShareAccountEdits implements Resolve<any> {
private sharesService = inject(SharesService);
resolve(): Observable<any> {
return this.sharesService.getAllPendingShareAccountEdits();
}
}
+12 -1
View File
@@ -29,6 +29,7 @@ import { TerminatedAccountComponent } from './checker-inbox-and-tasks-tabs/termi
import { PendingSavingsProductsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component';
import { ChequebookRequestsComponent } from './checker-inbox-and-tasks-tabs/chequebook-requests/chequebook-requests.component';
import { PendingSavingsAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component';
import { PendingShareAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component';
import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inbox.component';
/** Custom Resolvers */
@@ -46,6 +47,7 @@ import { GetTerminatedAccounts } from './common-resolvers/getTerminatedAccounts.
import { GetPendingSavingsProducts } from './common-resolvers/getPendingSavingsProducts.resolver';
import { GetPendingSavingsAccountEdits } from './common-resolvers/getPendingSavingsAccountEdits.resolver';
import { GetPendingCurrentAccountEdits } from './common-resolvers/getPendingCurrentAccountEdits.resolver';
import { GetPendingShareAccountEdits } from './common-resolvers/getPendingShareAccountEdits.resolver';
/** Tasks Routes */
const routes: Routes = [
@@ -142,6 +144,14 @@ const routes: Routes = [
path: 'pending-current-account-edits',
redirectTo: 'pending-savings-account-edits'
},
{
path: 'pending-share-account-edits',
component: PendingShareAccountEditsComponent,
data: { title: 'Pending Share Account Edits' },
resolve: {
pendingShareAccountEditsData: GetPendingShareAccountEdits
}
},
{
path: 'withdrawal-requests',
component: WithdrawalRequestsComponent,
@@ -221,7 +231,8 @@ const routes: Routes = [
GetTerminatedAccounts,
GetPendingSavingsProducts,
GetPendingSavingsAccountEdits,
GetPendingCurrentAccountEdits
GetPendingCurrentAccountEdits,
GetPendingShareAccountEdits
]
})
export class TasksRoutingModule {}