Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora into dev-latest
This commit is contained in:
+1
-1
@@ -144,7 +144,7 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (savingProductFormValid && selectedTabIndex === totalTabs - 1) {
|
@if (savingProductFormValid && selectedTabIndex === totalTabs - 1) {
|
||||||
<button mat-raised-button color="primary" (click)="submit()">
|
<button mat-raised-button color="primary" [disabled]="submitting" (click)="submit()">
|
||||||
<i class="fas fa-check" style="margin-right: 8px"></i>
|
<i class="fas fa-check" style="margin-right: 8px"></i>
|
||||||
{{ 'labels.buttons.Submit' | translate }}
|
{{ 'labels.buttons.Submit' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+13
-1
@@ -70,6 +70,10 @@ export class CreateSavingProductComponent {
|
|||||||
savingProductsTemplate: any;
|
savingProductsTemplate: any;
|
||||||
accountingRuleData: string[] = [];
|
accountingRuleData: string[] = [];
|
||||||
selectedTabIndex = 0;
|
selectedTabIndex = 0;
|
||||||
|
/** Guards against repeat clicks on Submit: the product name is unique in the database, so a
|
||||||
|
* second request for the same form fails with a duplicate key error even though the first one
|
||||||
|
* created the product. */
|
||||||
|
submitting = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ActivatedRoute} route Activated Route.
|
* @param {ActivatedRoute} route Activated Route.
|
||||||
@@ -162,8 +166,11 @@ export class CreateSavingProductComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
|
if (this.submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = true;
|
||||||
const savingProduct = this.prepareSavingProductPayload();
|
const savingProduct = this.prepareSavingProductPayload();
|
||||||
console.log('Saving product submit payload:', savingProduct);
|
|
||||||
this.productsService.createSavingProduct(savingProduct).subscribe({
|
this.productsService.createSavingProduct(savingProduct).subscribe({
|
||||||
next: (response: any) => {
|
next: (response: any) => {
|
||||||
if (response?.rollbackTransaction) {
|
if (response?.rollbackTransaction) {
|
||||||
@@ -181,6 +188,11 @@ export class CreateSavingProductComponent {
|
|||||||
{ relativeTo: this.route }
|
{ relativeTo: this.route }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
// the error itself is reported by the global error handler; just let the user correct the
|
||||||
|
// form and try again
|
||||||
|
this.submitting = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
-99
@@ -130,103 +130,4 @@
|
|||||||
<p class="empty-text">There are no accounts awaiting approval at this time.</p>
|
<p class="empty-text">There are no accounts awaiting approval at this time.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Share accounts awaiting approval render here too, rather than on their own tab. Own table,
|
|
||||||
own per-row approve/reject/view actions - unchanged from their previous standalone screen. -->
|
|
||||||
<h3 class="pa-section-heading">Pending Share Accounts</h3>
|
|
||||||
@if (showShareData) {
|
|
||||||
<div class="psa-action-bar">
|
|
||||||
<mat-form-field appearance="outline" class="psa-search-field">
|
|
||||||
<mat-icon matPrefix>search</mat-icon>
|
|
||||||
<input
|
|
||||||
#shareFilterInput
|
|
||||||
matInput
|
|
||||||
placeholder="Search by account number, member or product"
|
|
||||||
(keyup)="applyShareFilter(shareFilterInput.value)"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container">
|
|
||||||
<table mat-table [dataSource]="shareDataSource" class="modern-table">
|
|
||||||
<ng-container matColumnDef="accountNo">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Account No.</th>
|
|
||||||
<td mat-cell *matCellDef="let account" class="psa-mono">{{ account.accountNo }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="clientName">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Member</th>
|
|
||||||
<td mat-cell *matCellDef="let account">
|
|
||||||
{{ account.clientName }}
|
|
||||||
@if (account.officeName) {
|
|
||||||
<div class="psa-sub">{{ account.officeName }}</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="productName">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Product</th>
|
|
||||||
<td mat-cell *matCellDef="let account">{{ account.productName }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="requestedShares">
|
|
||||||
<th mat-header-cell *matHeaderCellDef class="ta-right">Shares</th>
|
|
||||||
<td mat-cell *matCellDef="let account" class="ta-right psa-num">{{ account.requestedShares }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="requestedAmount">
|
|
||||||
<th mat-header-cell *matHeaderCellDef class="ta-right">Amount</th>
|
|
||||||
<td mat-cell *matCellDef="let account" class="ta-right psa-num">
|
|
||||||
{{ account.requestedAmount | number: '1.2-2' }}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="resolution">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Resolution</th>
|
|
||||||
<td mat-cell *matCellDef="let account">
|
|
||||||
{{ account.resolutionNumber || '—' }}
|
|
||||||
@if (account.resolutionDate) {
|
|
||||||
<div class="psa-sub">{{ formatShareDate(account.resolutionDate) }}</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="submittedDate">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Submitted</th>
|
|
||||||
<td mat-cell *matCellDef="let account">
|
|
||||||
{{ formatShareDate(account.submittedDate) }}
|
|
||||||
@if (account.submittedByUsername) {
|
|
||||||
<div class="psa-sub">by {{ account.submittedByUsername }}</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
|
||||||
<td mat-cell *matCellDef="let account">
|
|
||||||
<button mat-stroked-button class="psa-btn psa-btn-view" (click)="viewShareAccount(account)">
|
|
||||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>View
|
|
||||||
</button>
|
|
||||||
<button mat-stroked-button class="psa-btn psa-btn-approve" (click)="approveShareAccount(account)">
|
|
||||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
|
||||||
</button>
|
|
||||||
<button mat-stroked-button class="psa-btn psa-btn-reject" (click)="rejectShareAccount(account)">
|
|
||||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="shareDisplayedColumns"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: shareDisplayedColumns" class="table-row"></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!showShareData) {
|
|
||||||
<div class="empty-state">
|
|
||||||
<fa-icon icon="hourglass-half" class="empty-icon"></fa-icon>
|
|
||||||
<p class="empty-title">No pending share accounts</p>
|
|
||||||
<p class="empty-text">There are no share accounts awaiting approval at this time.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-10
@@ -14,16 +14,6 @@ $text-muted: #64748b;
|
|||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Marks where the Share Account section begins below the normal-account table on this same screen. */
|
|
||||||
.pa-section-heading {
|
|
||||||
margin: 28px 0 12px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0d1b2a;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid $border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cv-action-bar {
|
.cv-action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+3
-120
@@ -29,24 +29,17 @@ import { MatCheckbox } from '@angular/material/checkbox';
|
|||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
|
||||||
import { TasksService } from '../../tasks.service';
|
import { TasksService } from '../../tasks.service';
|
||||||
import { SharesService } from 'app/shares/shares.service';
|
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
import { Dates } from 'app/core/utils/dates';
|
import { Dates } from 'app/core/utils/dates';
|
||||||
import { DecimalPipe } from '@angular/common';
|
|
||||||
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
|
||||||
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
||||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||||
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
|
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
|
||||||
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-pending-account',
|
selector: 'mifosx-pending-account',
|
||||||
templateUrl: './pending-account.component.html',
|
templateUrl: './pending-account.component.html',
|
||||||
styleUrls: [
|
styleUrls: ['./pending-account.component.scss'],
|
||||||
'./pending-account.component.scss',
|
|
||||||
'../pending-share-accounts/pending-share-accounts.component.scss'
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
FaIconComponent,
|
FaIconComponent,
|
||||||
@@ -61,8 +54,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
MatHeaderRow,
|
MatHeaderRow,
|
||||||
MatRowDef,
|
MatRowDef,
|
||||||
MatRow,
|
MatRow,
|
||||||
MatIcon,
|
MatIcon
|
||||||
DecimalPipe
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PendingAccountComponent {
|
export class PendingAccountComponent {
|
||||||
@@ -72,7 +64,6 @@ export class PendingAccountComponent {
|
|||||||
private dateUtils = inject(Dates);
|
private dateUtils = inject(Dates);
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private tasksService = inject(TasksService);
|
private tasksService = inject(TasksService);
|
||||||
private sharesService = inject(SharesService);
|
|
||||||
|
|
||||||
dataSource: MatTableDataSource<any>;
|
dataSource: MatTableDataSource<any>;
|
||||||
selection: SelectionModel<any>;
|
selection: SelectionModel<any>;
|
||||||
@@ -90,26 +81,8 @@ export class PendingAccountComponent {
|
|||||||
'status'
|
'status'
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Share accounts awaiting approval - rendered as their own section on this same screen rather
|
|
||||||
* than a separate tab. Kept out of `dataSource`/`selection` (bulk-select + date-picker approve)
|
|
||||||
* since a share subscription is approved/rejected one at a time against today's business date,
|
|
||||||
* exactly as it always was on its own page - only where it's shown has changed. */
|
|
||||||
shareDataSource: MatTableDataSource<any>;
|
|
||||||
showShareData = false;
|
|
||||||
|
|
||||||
shareDisplayedColumns: string[] = [
|
|
||||||
'accountNo',
|
|
||||||
'clientName',
|
|
||||||
'productName',
|
|
||||||
'requestedShares',
|
|
||||||
'requestedAmount',
|
|
||||||
'resolution',
|
|
||||||
'submittedDate',
|
|
||||||
'actions'
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.route.data.subscribe((data: { pendingAccountsData: any; pendingShareAccountsData: any }) => {
|
this.route.data.subscribe((data: { pendingAccountsData: any }) => {
|
||||||
// status 100 = submitted and pending approval only; exclude terminated accounts
|
// status 100 = submitted and pending approval only; exclude terminated accounts
|
||||||
const items = (data.pendingAccountsData?.pageItems || []).filter(
|
const items = (data.pendingAccountsData?.pageItems || []).filter(
|
||||||
(a: any) => a.status?.id === 100 && !a.terminateTrue
|
(a: any) => a.status?.id === 100 && !a.terminateTrue
|
||||||
@@ -117,10 +90,6 @@ export class PendingAccountComponent {
|
|||||||
this.showData = items.length > 0;
|
this.showData = items.length > 0;
|
||||||
this.dataSource = new MatTableDataSource(items);
|
this.dataSource = new MatTableDataSource(items);
|
||||||
this.selection = new SelectionModel(true, []);
|
this.selection = new SelectionModel(true, []);
|
||||||
|
|
||||||
const shareItems = data.pendingShareAccountsData || [];
|
|
||||||
this.showShareData = shareItems.length > 0;
|
|
||||||
this.shareDataSource = new MatTableDataSource(shareItems);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,92 +283,6 @@ export class PendingAccountComponent {
|
|||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyShareFilter(filterValue: string = '') {
|
|
||||||
this.shareDataSource.filter = filterValue.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A date from the share account API rendered readably, whether it arrives as a string or as
|
|
||||||
* Fineract's {@code [yyyy, m, d]} array. Kept separate from `formatDate()` above, which the
|
|
||||||
* normal-account table already relies on with its own date-array handling. */
|
|
||||||
formatShareDate(date: any): string {
|
|
||||||
if (!date) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const d = Array.isArray(date) ? new Date(date[0], date[1] - 1, date[2]) : new Date(date);
|
|
||||||
return isNaN(d.getTime()) ? String(date) : d.toLocaleDateString('en-IN');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Opens the share account itself, for a checker who wants more than the row shows. */
|
|
||||||
viewShareAccount(account: any): void {
|
|
||||||
this.router.navigate([
|
|
||||||
'/clients',
|
|
||||||
account.clientId,
|
|
||||||
'shares-accounts',
|
|
||||||
account.shareAccountId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
approveShareAccount(account: any): void {
|
|
||||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
|
||||||
data: {
|
|
||||||
heading: 'Approve Share Account',
|
|
||||||
dialogContext:
|
|
||||||
`Approve share account "${account.accountNo}" for ${account.clientName} — ` +
|
|
||||||
`${account.requestedShares} shares at ${account.requestedAmount}?`,
|
|
||||||
type: 'basic'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialogRef.afterClosed().subscribe((response: any) => {
|
|
||||||
if (response?.confirm) {
|
|
||||||
// The approval date is the business date rather than the checker's choice, same as this
|
|
||||||
// queue's original standalone screen: clearing a submission is a decision, not a backdated
|
|
||||||
// transaction, so a date field per row would be extra clicks for no benefit.
|
|
||||||
const data = {
|
|
||||||
approvedDate: this.dateUtils.formatDate(this.settingsService.businessDate, this.settingsService.dateFormat),
|
|
||||||
dateFormat: this.settingsService.dateFormat,
|
|
||||||
locale: this.settingsService.language.code
|
|
||||||
};
|
|
||||||
this.sharesService
|
|
||||||
.executeSharesAccountCommand(account.shareAccountId, 'approve', data)
|
|
||||||
.subscribe(() => this.reload());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectShareAccount(account: 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 Share Account ${account.accountNo}`,
|
|
||||||
formfields
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialogRef.afterClosed().subscribe((response: any) => {
|
|
||||||
if (response?.data) {
|
|
||||||
const data: any = {
|
|
||||||
rejectedDate: this.dateUtils.formatDate(this.settingsService.businessDate, this.settingsService.dateFormat),
|
|
||||||
dateFormat: this.settingsService.dateFormat,
|
|
||||||
locale: this.settingsService.language.code
|
|
||||||
};
|
|
||||||
const reason = response.data.value?.rejectReason?.trim();
|
|
||||||
if (reason) {
|
|
||||||
data.note = reason;
|
|
||||||
}
|
|
||||||
this.sharesService
|
|
||||||
.executeSharesAccountCommand(account.shareAccountId, 'reject', data)
|
|
||||||
.subscribe(() => this.reload());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
const url = this.router.url;
|
const url = this.router.url;
|
||||||
this.router
|
this.router
|
||||||
|
|||||||
@@ -72,6 +72,15 @@
|
|||||||
<fa-icon icon="hourglass-half" class="ac-tab__ic"></fa-icon>
|
<fa-icon icon="hourglass-half" class="ac-tab__ic"></fa-icon>
|
||||||
<span>Pending Account</span>
|
<span>Pending Account</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
class="ac-tab"
|
||||||
|
routerLink="./pending-share-accounts"
|
||||||
|
routerLinkActive="ac-tab--active"
|
||||||
|
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||||
|
>
|
||||||
|
<fa-icon icon="hourglass-half" class="ac-tab__ic"></fa-icon>
|
||||||
|
<span>Pending Share Accounts</span>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
class="ac-tab"
|
class="ac-tab"
|
||||||
routerLink="./product-approval"
|
routerLink="./product-approval"
|
||||||
|
|||||||
@@ -108,10 +108,7 @@ const routes: Routes = [
|
|||||||
component: PendingAccountComponent,
|
component: PendingAccountComponent,
|
||||||
data: { title: 'Pending Account' },
|
data: { title: 'Pending Account' },
|
||||||
resolve: {
|
resolve: {
|
||||||
pendingAccountsData: GetPendingAccounts,
|
pendingAccountsData: GetPendingAccounts
|
||||||
// Share accounts awaiting approval now render inside this same screen instead of their
|
|
||||||
// own tab - reuses the same resolver the standalone route below still uses.
|
|
||||||
pendingShareAccountsData: GetPendingShareAccounts
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user