Merge branch 'maker-checker' into dev: Product Approval workflow
Publish Image in Docker Hub / build (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

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:
2026-09-11 11:47:51 +05:30
25 changed files with 735 additions and 119 deletions
@@ -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;
}
}