feat(dds): upgrade DDS UI - agent, party, daily entry, approvals, product view

- Agent master: tabbed layout, customer search dialog, GL dropdown, account card, view page
- Party master: tabbed layout, expected party code, nominee cards, relation from code values
- Daily transaction entry: compact style, collections popup, Apply DDS Receipt placement
- Approvals: new review modal, Agent Approval tab, DDS routing into product/pending account
- DDS product: view page and list buttons; drop Auto Interest Post Day

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 10:50:03 +05:30
co-authored by Claude Sonnet 5.5
parent 357f666421
commit 293b671966
45 changed files with 5973 additions and 1292 deletions
@@ -0,0 +1,125 @@
<!--
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="psp-tab-content">
@if (dataSource.data.length > 0) {
<div class="psp-action-bar">
<mat-form-field appearance="outline" class="psp-search-field">
<mat-icon matPrefix>search</mat-icon>
<input
#filterInput
matInput
placeholder="Search by agent, client, branch or account"
(keyup)="applyFilter(filterInput.value)"
/>
</mat-form-field>
</div>
<div class="table-container">
<table mat-table [dataSource]="dataSource" class="modern-table">
<ng-container matColumnDef="request">
<th mat-header-cell *matHeaderCellDef>Request</th>
<td mat-cell *matCellDef="let row">
<span class="status-badge status-badge--action" [class.status-badge--delete]="row.action === 'CLOSE'">{{
requestLabel(row)
}}</span>
</td>
</ng-container>
<ng-container matColumnDef="agent">
<th mat-header-cell *matHeaderCellDef>Agent</th>
<td mat-cell *matCellDef="let row">
<strong>{{ row.agentName || '—' }}</strong>
<!-- A new agent has no code until it is approved. -->
<div class="cell-mono">{{ row.agentCode || 'Code on approval' }}</div>
</td>
</ng-container>
<ng-container matColumnDef="client">
<th mat-header-cell *matHeaderCellDef>Client</th>
<td mat-cell *matCellDef="let row">{{ row.clientName || '—' }}</td>
</ng-container>
<ng-container matColumnDef="branch">
<th mat-header-cell *matHeaderCellDef>Branch</th>
<td mat-cell *matCellDef="let row">{{ row.officeName || '—' }}</td>
</ng-container>
<ng-container matColumnDef="savingAccount">
<th mat-header-cell *matHeaderCellDef>Saving A/C</th>
<td mat-cell *matCellDef="let row" class="cell-mono">{{ row.savingAccountNo || '—' }}</td>
</ng-container>
<ng-container matColumnDef="guarantors">
<th mat-header-cell *matHeaderCellDef>Guarantors</th>
<td mat-cell *matCellDef="let row">{{ row.guarantorCount ?? 0 }}</td>
</ng-container>
<ng-container matColumnDef="maker">
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
<td mat-cell *matCellDef="let row">{{ row.makerName || row.maker }}</td>
</ng-container>
<ng-container matColumnDef="madeOn">
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
<td mat-cell *matCellDef="let row">{{ formatDate(row.submittedOn) }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let row">
<span class="status-badge status-badge--pending">{{ row.status }}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let row">
<div class="aa-actions">
<button mat-stroked-button class="psp-btn psp-btn-view" (click)="view(row)">
<fa-icon icon="eye" class="m-r-10"></fa-icon>View
</button>
<!-- Not offered to whoever made the request - another user with checker rights has to act. -->
<button
mat-stroked-button
class="psp-btn psp-btn-approve"
[disabled]="!canAct(row)"
[title]="isMine(row) ? 'You made this request, so another user has to approve it' : ''"
(click)="approve(row)"
>
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
</button>
<button
mat-stroked-button
class="psp-btn psp-btn-reject"
[disabled]="!canAct(row)"
[title]="isMine(row) ? 'You made this request, so another user has to reject it' : ''"
(click)="reject(row)"
>
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
</button>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
</table>
</div>
} @else if (loading) {
<div class="empty-state">
<p class="empty-title">Loading agent requests…</p>
</div>
} @else {
<div class="empty-state">
<fa-icon icon="user-tie" class="empty-icon"></fa-icon>
<p class="empty-title">No agent requests waiting</p>
<p class="empty-text">There are no agents awaiting approval at this time.</p>
</div>
}
</div>
@@ -0,0 +1,30 @@
/**
* 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/.
*/
// What the Agent Approval tab adds to the Product Approval sheet it shares.
// View, Approve and Reject sit on one line: the column takes the width its buttons need and the
// other columns - whose names and addresses can wrap - give way.
.aa-actions {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
white-space: nowrap;
.psp-btn {
flex: 0 0 auto;
margin-right: 0;
}
}
th.mat-column-actions,
td.mat-column-actions {
width: 1%;
white-space: nowrap;
}
@@ -0,0 +1,175 @@
/**
* 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 {
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 { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { OrganizationService } from 'app/organization/organization.service';
import { AuthenticationService } from 'app/core/authentication/authentication.service';
import { DdsApprovalReviewComponent } from 'app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component';
import { ddsMadeOn } from '../../product-entities';
/**
* Approvals › Agent Approval: every DDS agent request waiting on a checker - an agent being opened,
* changed or closed.
*
* An agent has no voucher and no amount, so the money columns of DDS Approvals say nothing about one.
* This lists them by what identifies an agent - who they are, which branch, which savings account the
* commission goes into, how many guarantors - and opens the same review the DDS queue uses, which
* shows the agent and their guarantors in full and approves or rejects through the DDS queue.
*/
@Component({
selector: 'mifosx-agent-approvals',
templateUrl: './agent-approvals.component.html',
// The same look as Product Approval, so the tabs of this screen read alike.
styleUrls: [
'../pending-savings-products/pending-savings-products.component.scss',
'./agent-approvals.component.scss'
],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatIcon
]
})
export class AgentApprovalsComponent implements OnInit {
private organizationService = inject(OrganizationService);
private authenticationService = inject(AuthenticationService);
private dialog = inject(MatDialog);
private readonly currentUserId = this.authenticationService.getCredentials()?.userId;
dataSource = new MatTableDataSource<any>([]);
loading = true;
displayedColumns: string[] = [
'request',
'agent',
'client',
'branch',
'savingAccount',
'guarantors',
'maker',
'madeOn',
'status',
'actions'
];
ngOnInit(): void {
this.dataSource.filterPredicate = (row: any, filter: string) =>
[
row.agentName,
row.agentCode,
row.clientName,
row.officeName,
row.savingAccountNo,
row.type,
row.maker,
row.makerName
]
.filter(Boolean)
.join(' ')
.toLowerCase()
.includes(filter);
this.load();
}
load(): void {
this.loading = true;
this.organizationService
.getDdsApprovals('PENDING')
.pipe(catchError(() => of([])))
.subscribe((rows: any) => {
const agents = (Array.isArray(rows) ? rows : []).filter((row: any) => row?.entity === 'AGENT');
this.dataSource.data = agents.map((row: any) => ({ ...row, submittedOn: ddsMadeOn(row.madeOn) }));
this.loading = false;
});
}
applyFilter(value: string = ''): void {
this.dataSource.filter = value.trim().toLowerCase();
}
/** What the request is, as the badge reads it. */
requestLabel(row: any): string {
return row.action === 'CREATE' ? 'New' : row.action === 'CLOSE' ? 'Close' : 'Edit';
}
isMine(row: any): boolean {
return !!this.currentUserId && Number(row.makerId) === Number(this.currentUserId);
}
formatDate(date: Date | null): string {
return date ? date.toLocaleString('en-IN') : '';
}
/** Opens the agent in full - the client, address, branch, accounts and guarantors the request carries. */
view(row: any): void {
this.dialog
.open(DdsApprovalReviewComponent, {
data: { entry: row, currentUserId: this.currentUserId },
autoFocus: false
})
.afterClosed()
.subscribe((done: boolean) => {
if (done) {
this.load();
}
});
}
/**
* Whether this user may approve or reject the request. It is the same act the review and DDS
* Approvals perform, through the DDS queue, which refuses whoever made the request - so it is not
* offered to them here either.
*/
canAct(row: any): boolean {
return !this.isMine(row);
}
/**
* Approve and Reject open the agent in full rather than acting at once: a request is never actioned
* without its client, address, branch, accounts and guarantors on screen. The review carries the
* Approve and Reject buttons, and asks before rejecting.
*/
approve(row: any): void {
this.view(row);
}
reject(row: any): void {
this.view(row);
}
}
@@ -40,7 +40,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatCheckbox } from '@angular/material/checkbox';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { isProductEntity, isTempTableAccountEdit } from '../../product-entities';
import { isDdsRoutedRequest, isProductEntity, isTempTableAccountEdit } from '../../product-entities';
@Component({
selector: 'mifosx-checker-inbox',
@@ -118,7 +118,11 @@ export class CheckerInboxComponent implements OnInit {
*/
private excludeRoutedElsewhere(items: any[]): any[] {
return (items || []).filter(
(item: any) => !isProductEntity(item?.entityName) && !isTempTableAccountEdit(item?.entityName, item?.actionName)
(item: any) =>
!isProductEntity(item?.entityName) &&
!isTempTableAccountEdit(item?.entityName, item?.actionName) &&
// DDS product and account requests are listed under Product Approval and Pending Account.
!isDdsRoutedRequest(item?.entityName, item?.actionName)
);
}
@@ -61,7 +61,10 @@
<ng-container matColumnDef="accountNo">
<th mat-header-cell *matHeaderCellDef>Account No</th>
<td mat-cell *matCellDef="let account" class="cell-mono">{{ account.accountNo }}</td>
<!-- A new DDS account has no number until it is approved. -->
<td mat-cell *matCellDef="let account" class="cell-mono">
{{ account.accountNo || (account.source === 'DDS' ? 'On approval' : '') }}
</td>
</ng-container>
<ng-container matColumnDef="clientName">
@@ -35,6 +35,9 @@ import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.componen
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { OrganizationService } from 'app/organization/organization.service';
import { AuthenticationService } from 'app/core/authentication/authentication.service';
import { DdsApprovalReviewComponent } from 'app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component';
@Component({
selector: 'mifosx-pending-account',
@@ -64,6 +67,8 @@ export class PendingAccountComponent {
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private tasksService = inject(TasksService);
private organizationService = inject(OrganizationService);
private authenticationService = inject(AuthenticationService);
dataSource: MatTableDataSource<any>;
selection: SelectionModel<any>;
@@ -110,7 +115,27 @@ export class PendingAccountComponent {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
/** A DDS account opening or edit, which waits in - and is actioned through - the DDS approval queue. */
private isDds(account: any): boolean {
return account?.source === 'DDS';
}
navigateToAccount(account: any): void {
if (this.isDds(account)) {
// A new DDS account has no page until it is approved, so it opens the DDS review instead.
this.dialog
.open(DdsApprovalReviewComponent, {
data: { entry: account.ddsEntry, currentUserId: this.authenticationService.getCredentials()?.userId },
autoFocus: false
})
.afterClosed()
.subscribe((done: boolean) => {
if (done) {
this.reload();
}
});
return;
}
const clientId = account.clientId;
const accountId = account.id;
const depositTypeId = account.depositType?.id;
@@ -190,7 +215,9 @@ export class PendingAccountComponent {
selected.forEach((account: any) => {
const depositTypeId = account.depositType?.id;
let reject$;
if (account.accountType === 'Current') {
if (this.isDds(account)) {
reject$ = this.organizationService.actOnDdsApproval(account.id, 'reject');
} else if (account.accountType === 'Current') {
reject$ = this.tasksService.rejectCurrentAccount(account.id, payload);
} else if (depositTypeId === 200) {
reject$ = this.tasksService.rejectFDAccount(account.id, payload);
@@ -223,6 +250,10 @@ export class PendingAccountComponent {
* approving from inside an individual account (see e.g. ApproveSavingsAccountComponent).
*/
private approveAndActivate$(account: any, approvedOnDate: string, dateFormat: string, locale: string) {
// A DDS account opens active once its request is approved; there is no separate activation step.
if (this.isDds(account)) {
return this.organizationService.actOnDdsApproval(account.id, 'approve');
}
const approvePayload = { approvedOnDate, dateFormat, locale };
const activatePayload = { activatedOnDate: approvedOnDate, dateFormat, locale };
const depositTypeId = account.depositType?.id;
@@ -35,6 +35,8 @@ import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { productTypeDisplayLabel } from '../../product-entities';
import { AuthenticationService } from 'app/core/authentication/authentication.service';
import { DdsApprovalReviewComponent } from 'app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component';
@Component({
selector: 'mifosx-pending-savings-products',
@@ -62,6 +64,7 @@ export class PendingSavingsProductsComponent {
private dialog = inject(MatDialog);
private productsService = inject(ProductsService);
private tasksService = inject(TasksService);
private authenticationService = inject(AuthenticationService);
dataSource: MatTableDataSource<any>;
showData = false;
@@ -173,6 +176,10 @@ export class PendingSavingsProductsComponent {
* renders the submitted payload and can action the entry itself.
*/
viewDiff(row: any): void {
if (this.isDds(row)) {
this.openDdsReview(row);
return;
}
if (!this.isPendingEdit(row)) {
this.dialog
.open(ViewCheckerInboxComponent, {
@@ -212,7 +219,35 @@ export class PendingSavingsProductsComponent {
this.loadingDiffFor = null;
}
/** A DDS product request, which is read and actioned through the DDS approval queue. */
isDds(row: any): boolean {
return row?.source === 'DDS';
}
/**
* The DDS review shows the product's terms - against today's on an edit - and approves or rejects
* it there, refusing the maker as the DDS queue does. Approve and Reject on the row open it too, so
* a DDS product is never actioned without its terms on screen.
*/
private openDdsReview(row: any): void {
this.dialog
.open(DdsApprovalReviewComponent, {
data: { entry: row.ddsEntry, currentUserId: this.authenticationService.getCredentials()?.userId },
autoFocus: false
})
.afterClosed()
.subscribe((done: boolean) => {
if (done) {
this.reload();
}
});
}
approve(row: any): void {
if (this.isDds(row)) {
this.openDdsReview(row);
return;
}
const isEdit = this.isPendingEdit(row);
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
@@ -234,6 +269,10 @@ export class PendingSavingsProductsComponent {
}
reject(row: any): void {
if (this.isDds(row)) {
this.openDdsReview(row);
return;
}
if (!this.isPendingEdit(row)) {
// The native /makercheckers reject endpoint takes no payload, so there is nowhere to store a
// reason - ask for plain confirmation rather than collecting text that would be discarded.
@@ -271,6 +271,13 @@
<span class="ac-tab__count" title="Waiting for action">{{ count('dds-approvals') }}</span>
}
</a>
<a class="ac-tab" routerLink="./agent-approvals" routerLinkActive="ac-tab--active">
<fa-icon icon="user-tie" class="ac-tab__ic"></fa-icon>
<span>Agent Approval</span>
@if (count('agent-approvals')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('agent-approvals') }}</span>
}
</a>
<a
class="ac-tab"
routerLink="./chequebook-requests"
@@ -10,17 +10,21 @@ import { Injectable, inject } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable, forkJoin, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { OrganizationService } from 'app/organization/organization.service';
import { TasksService } from '../tasks.service';
import { ddsMadeOn, isDdsAccountEntry } from '../product-entities';
@Injectable()
export class GetPendingAccounts implements Resolve<any> {
private tasksService = inject(TasksService);
private organizationService = inject(OrganizationService);
resolve(): Observable<any> {
return forkJoin({
savings: this.tasksService.getPendingSavingsAccounts().pipe(catchError(() => of({ pageItems: [] }))),
current: this.tasksService.getPendingCurrentAccounts().pipe(catchError(() => of([]))),
offices: this.tasksService.getAllOffices().pipe(catchError(() => of([])))
offices: this.tasksService.getAllOffices().pipe(catchError(() => of([]))),
dds: this.organizationService.getDdsApprovals('PENDING').pipe(catchError(() => of([])))
}).pipe(
map((results) => {
const officeMap: Record<number, string> = {};
@@ -39,9 +43,34 @@ export class GetPendingAccounts implements Resolve<any> {
(a: any) => ({ ...a, accountType: 'Current' })
);
/*
* DDS account openings and edits wait in the DDS approval queue rather than as submitted
* accounts, so they are laid onto the same columns here. The status filter in the component
* reads status.id 100, which these carry so they are kept like any other pending account.
*/
const ddsItems = (Array.isArray(results.dds) ? results.dds : [])
.filter((entry: any) => isDdsAccountEntry(entry))
.map((entry: any) => ({
id: entry.id,
source: 'DDS',
ddsEntry: entry,
accountNo: entry.accountNo || '',
clientName: entry.clientName || entry.details,
productName: entry.productName,
accountType: entry.action === 'UPDATE' ? 'DDS · Edit' : 'DDS',
officeName: entry.officeName || '',
fieldOfficerName: entry.agentName || '',
timeline: { submittedOnDate: ddsMadeOn(entry.madeOn) },
status: {
id: 100,
value: entry.action === 'UPDATE' ? 'Edit pending approval' : 'Submitted and pending approval'
}
}));
return { pageItems: [
...savingsItems,
...currentItems
...currentItems,
...ddsItems
] };
})
);
@@ -11,8 +11,15 @@ import { Resolve } from '@angular/router';
import { Observable, forkJoin, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ProductsService } from 'app/products/products.service';
import { OrganizationService } from 'app/organization/organization.service';
import { TasksService } from '../tasks.service';
import { isProductEntity, isTempTableProductEdit, PRODUCT_ENTITY_TYPE_LABEL } from '../product-entities';
import {
ddsMadeOn,
isDdsProductEntry,
isProductEntity,
isTempTableProductEdit,
PRODUCT_ENTITY_TYPE_LABEL
} from '../product-entities';
/**
* Builds the Approvals -> Product Approval list from the two pending-request sources the platform
@@ -32,6 +39,7 @@ import { isProductEntity, isTempTableProductEdit, PRODUCT_ENTITY_TYPE_LABEL } fr
export class GetPendingSavingsProducts implements Resolve<any> {
private productsService = inject(ProductsService);
private tasksService = inject(TasksService);
private organizationService = inject(OrganizationService);
resolve(): Observable<any> {
return forkJoin({
@@ -42,7 +50,8 @@ export class GetPendingSavingsProducts implements Resolve<any> {
rd: this.productsService.getPendingRecurringDepositProductEdits().pipe(catchError(() => of([]))),
// includeJson so a CREATE row - which has no resourceId, and therefore no product to look up -
// can still show the submitted product name.
makerChecker: this.tasksService.getMakerCheckerData({ includeJson: true }).pipe(catchError(() => of([])))
makerChecker: this.tasksService.getMakerCheckerData({ includeJson: true }).pipe(catchError(() => of([]))),
dds: this.organizationService.getDdsApprovals('PENDING').pipe(catchError(() => of([])))
}).pipe(
map((results) => [
...this.asEditRows(results.savings, 'SAVINGS'),
@@ -50,11 +59,31 @@ export class GetPendingSavingsProducts implements Resolve<any> {
...this.asEditRows(results.share, 'SHARE'),
...this.asEditRows(results.fd, 'FD'),
...this.asEditRows(results.rd, 'RD'),
...this.asMakerCheckerRows(results.makerChecker)
...this.asMakerCheckerRows(results.makerChecker),
...this.asDdsRows(results.dds)
])
);
}
/** DDS product requests, from the DDS queue - which is also how they are viewed and actioned. */
private asDdsRows(entries: any): any[] {
return (Array.isArray(entries) ? entries : [])
.filter((entry: any) => isDdsProductEntry(entry))
.map((entry: any) => ({
id: entry.id,
source: 'DDS',
entityName: entry.entity,
actionName: entry.action,
productType: 'DDS',
savingsProductId: entry.resourceId ?? null,
savingsProductName: entry.details || entry.type,
makerUsername: entry.maker,
createdOn: ddsMadeOn(entry.madeOn),
status: 'PENDING',
ddsEntry: entry
}));
}
/** Staged product edits, unchanged apart from the source/action tags the merged table needs. */
private asEditRows(edits: any[], productType: string): any[] {
return (edits || []).map((edit: any) => ({
+49 -1
View File
@@ -56,6 +56,53 @@ export const TEMP_TABLE_ACCOUNT_ENTITIES: readonly string[] = [
'CURRENTACCOUNT'
];
/**
* DDS keeps its own approval queue (/dds/approvals), which also describes each entry in the clerk's
* terms. Its product and account requests are listed in Product Approval and Pending Account as well,
* so a checker finds them where every other product and account waits - but they are read from, and
* actioned through, that DDS queue rather than /makercheckers.
*/
export function isDdsProductEntry(entry: any): boolean {
return isDdsRoutedRequest(entry?.entity, entry?.action) && entry?.entity === 'DDSACCOUNTTYPE';
}
export function isDdsAccountEntry(entry: any): boolean {
return isDdsRoutedRequest(entry?.entity, entry?.action) && entry?.entity === 'PARTY';
}
/**
* Whether a command is a DDS product or DDS account create/edit - one listed under Product Approval or
* Pending Account, and so left out of DDS Approvals and the Checker Inbox, which would otherwise show
* the same request a second and third time.
*/
export function isDdsRoutedRequest(
entityName: string | undefined | null,
actionName: string | undefined | null
): boolean {
const entity = (entityName ?? '').toUpperCase();
// An agent request of any kind waits under Agent Approval: an agent has no voucher and no amount,
// so the money columns of DDS Approvals say nothing about one.
if (entity === 'AGENT') {
return true;
}
return [
'DDSACCOUNTTYPE',
'PARTY'
].includes(entity) && [
'CREATE',
'UPDATE'
].includes((actionName ?? '').toUpperCase());
}
/** The DDS queue's "2026-10-27 11:04" as a Date, for the tables' date columns. */
export function ddsMadeOn(madeOn: string | undefined | null): Date | null {
if (!madeOn) {
return null;
}
const d = new Date(String(madeOn).replace(' ', 'T'));
return isNaN(d.getTime()) ? null : d;
}
/** Short type badge shown in the Product Approval table, matching the pending-edit productType tags. */
export const PRODUCT_ENTITY_TYPE_LABEL: Record<string, string> = {
LOANPRODUCT: 'LOAN',
@@ -111,7 +158,8 @@ export const PRODUCT_TYPE_DISPLAY_LABEL: Record<string, string> = {
FD: 'FIXED DEPOSIT PRODUCT',
'FIXED DEPOSIT': 'FIXED DEPOSIT PRODUCT',
RD: 'RECURRING DEPOSIT PRODUCT',
'RECURRING DEPOSIT': 'RECURRING DEPOSIT PRODUCT'
'RECURRING DEPOSIT': 'RECURRING DEPOSIT PRODUCT',
DDS: 'DDS PRODUCT'
};
/** Label shown in the Product Approval Type column. Unmapped types render unchanged. */
+6
View File
@@ -56,6 +56,7 @@ import { GetPendingCurrentAccountEdits } from './common-resolvers/getPendingCurr
import { GetPendingShareAccountEdits } from './common-resolvers/getPendingShareAccountEdits.resolver';
import { GetPendingShareAccounts } from './common-resolvers/getPendingShareAccounts.resolver';
import { DdsApprovalsComponent } from '../deposits/dds-approvals/dds-approvals.component';
import { AgentApprovalsComponent } from './checker-inbox-and-tasks-tabs/agent-approvals/agent-approvals.component';
/** Tasks Routes */
const routes: Routes = [
@@ -235,6 +236,11 @@ const routes: Routes = [
component: DdsApprovalsComponent,
data: { title: 'DDS Approvals' }
},
{
path: 'agent-approvals',
component: AgentApprovalsComponent,
data: { title: 'Agent Approval' }
},
{
path: 'chequebook-requests',
component: ChequebookRequestsComponent,