Merge branch 'maker-checker' into dev: Product Approval workflow
Brings in the Product Approval feature (anshul.rathore) — a unified Approvals
screen for Loan/Savings/Current/Share/FD/RD/Charge product requests:
- Merges two existing pending-request sources into one list: staged product
edits (m_saving_product_temp, via /{producttype}/pending) and native
maker-checker entries (/makercheckers) for product CREATE/DELETE and
LOANPRODUCT/CHARGE of any action.
- Checker Inbox no longer lists product requests at all — they now live only
in Product Approval, so a checker isn't checking two queues for the same
thing. Clicking a non-product row now navigates straight to that entity's
own view page when one exists, instead of always opening a detail dialog.
- View Checker Inbox dialog gains a caller-supplied heading (used when opened
from Product Approval) and a notice steering staged product edits back to
Product Approval instead of being actioned in place.
- Route renamed pending-savings-products -> product-approval, with the old
path kept as a redirect for existing bookmarks/links.
8 files conflicted with dev's own recent Checker Inbox work (popup-based
detail view, e8097bc2/63134f17) and were resolved by hand, keeping both
sides' real functionality rather than picking one branch wholesale:
- product-edit-diff-dialog.component.{ts,html,scss}: kept dev's diff dialog
(resolves GL account ids to names, aliases fields like currencyCode against
the stored product, normalises the two date shapes the API uses) over
maker-checker's simpler raw-value version.
- pending-savings-products.component.{ts,html}: kept maker-checker's full
isPendingEdit()-based dispatch (dev's version was a strict subset of it).
- view-checker-inbox.component.{ts,html,scss}: kept dev's per-entry Delete
action and maker-checker's recursive Command-payload tree renderer and
custom heading side by side.
Also recovered two regressions the merge introduced silently (no conflict
markers, so `git merge` gave no warning):
- tasks.service.ts lost deleteMakerChecker() entirely — restored.
- Checker Inbox's bulk Delete action (button, deleteChecker()/
bulkDeleteChecker(), and its .cv-btn-delete style) was dropped across three
files — restored to match dev's pre-merge behaviour exactly.
And one duplicate the merge produced without a conflict marker: two
independent viewEntry() implementations landed in checker-inbox.component.ts
(TS2393, caught at build). Kept maker-checker's entity-view-page-routing
version — a superset of dev's dialog-only version — and dropped the duplicate,
along with a leftover diagnostic console.log.
This commit is contained in:
@@ -11,15 +11,27 @@ 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 { TasksService } from '../tasks.service';
|
||||
import { isProductEntity, isTempTableProductEdit, PRODUCT_ENTITY_TYPE_LABEL } from '../product-entities';
|
||||
|
||||
/**
|
||||
* Merges pending edits across all product types sharing the m_savings_product_temp backing table -
|
||||
* Savings, Current Account, Share, Fixed Deposit, and Recurring Deposit products - into one list for
|
||||
* the Pending Product Edits screen.
|
||||
* Builds the Approvals -> Product Approval list from the two pending-request sources the platform
|
||||
* already has. No new workflow or table is involved; both feeds are read as-is.
|
||||
*
|
||||
* 1. Pending product edits (m_saving_product_temp), via /{producttype}/pending. UPDATE only, for the
|
||||
* five product types whose PUT handler stages the change instead of logging a command.
|
||||
* 2. Native maker-checker entries (m_portfolio_command_source), via /makercheckers. This is where
|
||||
* product CREATE and DELETE land - and LOANPRODUCT/CHARGE requests of every action, since those
|
||||
* were never diverted to the staging table.
|
||||
*
|
||||
* Rows staged in (1) also have an audit-only command-source row that surfaces in (2), so the
|
||||
* maker-checker feed drops them - see isTempTableProductEdit. Each source keeps its own approve/reject
|
||||
* path in the component; the merge here is presentational only.
|
||||
*/
|
||||
@Injectable()
|
||||
export class GetPendingSavingsProducts implements Resolve<any> {
|
||||
private productsService = inject(ProductsService);
|
||||
private tasksService = inject(TasksService);
|
||||
|
||||
resolve(): Observable<any> {
|
||||
return forkJoin({
|
||||
@@ -27,15 +39,73 @@ export class GetPendingSavingsProducts implements Resolve<any> {
|
||||
current: this.productsService.getPendingCurrentProductEdits().pipe(catchError(() => of([]))),
|
||||
share: this.productsService.getPendingShareProductEdits().pipe(catchError(() => of([]))),
|
||||
fd: this.productsService.getPendingFixedDepositProductEdits().pipe(catchError(() => of([]))),
|
||||
rd: this.productsService.getPendingRecurringDepositProductEdits().pipe(catchError(() => of([])))
|
||||
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([])))
|
||||
}).pipe(
|
||||
map((results) => [
|
||||
...(results.savings || []).map((e: any) => ({ ...e, productType: 'SAVINGS' })),
|
||||
...(results.current || []).map((e: any) => ({ ...e, productType: 'CURRENT' })),
|
||||
...(results.share || []).map((e: any) => ({ ...e, productType: 'SHARE' })),
|
||||
...(results.fd || []).map((e: any) => ({ ...e, productType: 'FD' })),
|
||||
...(results.rd || []).map((e: any) => ({ ...e, productType: 'RD' }))
|
||||
...this.asEditRows(results.savings, 'SAVINGS'),
|
||||
...this.asEditRows(results.current, 'CURRENT'),
|
||||
...this.asEditRows(results.share, 'SHARE'),
|
||||
...this.asEditRows(results.fd, 'FD'),
|
||||
...this.asEditRows(results.rd, 'RD'),
|
||||
...this.asMakerCheckerRows(results.makerChecker)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/** 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) => ({
|
||||
...edit,
|
||||
productType,
|
||||
source: 'PENDING_EDIT',
|
||||
actionName: 'UPDATE'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalises maker-checker entries onto the same field names the pending-edit table already renders,
|
||||
* so the existing template keeps working for both sources.
|
||||
*/
|
||||
private asMakerCheckerRows(entries: any): any[] {
|
||||
const items = Array.isArray(entries) ? entries : (entries?.pageItems ?? []);
|
||||
return items
|
||||
.filter((entry: any) => isProductEntity(entry?.entityName))
|
||||
.filter((entry: any) => !isTempTableProductEdit(entry?.entityName, entry?.actionName))
|
||||
.map((entry: any) => ({
|
||||
id: entry.id,
|
||||
source: 'MAKER_CHECKER',
|
||||
entityName: entry.entityName,
|
||||
actionName: (entry.actionName ?? '').toUpperCase(),
|
||||
productType: PRODUCT_ENTITY_TYPE_LABEL[(entry.entityName ?? '').toUpperCase()] ?? entry.entityName,
|
||||
savingsProductId: entry.resourceId ?? null,
|
||||
savingsProductName: this.resolveName(entry),
|
||||
makerUsername: entry.maker,
|
||||
// /makercheckers returns madeOnDate as a Unix timestamp in seconds.
|
||||
// new Date(seconds) misreads it as milliseconds → January 1970.
|
||||
// Normalise to milliseconds here, matching DatetimeFormatPipe's unix() branch.
|
||||
createdOn:
|
||||
typeof entry.madeOnDate === 'number' && entry.madeOnDate < 1e12 ? entry.madeOnDate * 1000 : entry.madeOnDate,
|
||||
status: 'PENDING'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A CREATE has no resourceId - the product does not exist until approval - so the name comes from
|
||||
* the submitted command payload. Falls back to the entity/id when the payload is absent (a DELETE
|
||||
* carries none) or unparseable.
|
||||
*/
|
||||
private resolveName(entry: any): string {
|
||||
try {
|
||||
const payload = entry?.commandAsJson ? JSON.parse(entry.commandAsJson) : null;
|
||||
if (payload?.name) {
|
||||
return payload.name;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to the identifier below */
|
||||
}
|
||||
return entry?.resourceId != null ? `${entry.entityName} #${entry.resourceId}` : entry.entityName;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user