# Issue 748 Multi-Approver Record Detail Actions

Status: implementation-ready
Scope class: approval runtime UI contract
Date: 2026-05-14

## Purpose

Issue #748 exposes a mismatch between the approval runtime and the record
detail UI. The backend already creates one `ProcessInstanceWorkitem` per
resolved approver for a pending approval step, but the frontend approval chain
normalizes a pending step to a single actor/work item. Record detail actions
therefore become visible only to whichever approver happened to occupy that
single slot.

This design keeps approval behavior metadata-driven and generic. It does not
add object-specific branches, tenant policy, new approval modes, or new public
REST shapes.

## Current Failure

The current frontend path has three single-actor assumptions:

- `approvalChainBuilder.ts` maps pending work items to pending runtime nodes by
  array position and stores only one `actorId` and `workItemId` on the pending
  chain step.
- `useRecordApproval.ts` derives banner state from the first pending chain
  step and exposes only one `approverId` and `pendingWorkItemId`.
- `ApprovalBanner.tsx` and `ApprovalHistory.tsx` check the current user by
  single-ID equality, so sibling approvers do not see action controls from the
  record detail page.

The backend already has the important runtime semantics:

- Submit and step advancement can create multiple work items for one pending
  `ProcessInstanceNode`.
- `FirstResponse` approval clears all sibling work items and finalizes the
  step.
- `Unanimous` approval clears only the acting approver's work item until every
  approver has responded.
- Approve, reject, and reassign already authorize against the acting
  `ProcessInstanceWorkitem.ActorId`, so the frontend must pass the current
  user's work item rather than changing backend authorization.
- Recall already authorizes against `ProcessInstance.SubmittedById` and then
  deletes every pending sibling work item for the instance.

The remaining backend issue in #748 is targeted coverage for the metadata
step-name fallback used by `resolveApproverIDsForNodeWithContext`.

## Contract

- A pending approval chain step represents the runtime step, not a single
  approver. It may carry many pending assignments.
- Each pending assignment carries at least `workItemId`, `actorId`, and display
  name fields resolved from `Actor` or `OriginalActor`.
- Existing single-actor fields on `ApprovalChainStep` and
  `RecordApprovalStatus` remain populated from the first pending assignment for
  backwards-compatible display and recall behavior.
- Record detail action eligibility is determined by finding the current user's
  own pending assignment, not by comparing against the first pending actor.
- Approve, reject, and reassign actions use the current user's specific
  `ProcessInstanceWorkitem.Id`.
- Submitter recall may continue to use any pending work item from the active
  instance, because the backend authorizes recall by submitter and then removes
  the active approval.
- Single-approver steps preserve the existing UI behavior and public type
  shape.
- If a process has one pending runtime node, every pending work item returned
  for the process instance belongs to that active step. This is the normal
  NexusCRM approval runtime shape.
- If multiple pending runtime nodes ever appear without a work-item-to-node
  foreign key, the builder keeps the old positional fallback rather than
  pretending it can infer a missing relationship.

## Implementation Shape

Frontend:

- Add a `PendingApprovalAssignment` UI type and optional
  `pendingWorkItems` arrays to `ApprovalChainStep` and
  `RecordApprovalStatus`.
- Update `buildApprovalChain` to normalize all pending
  `ProcessInstanceWorkitem` records into assignment objects.
- For the ordinary active-step shape, attach the full assignment list to the
  pending chain step. Continue filling `actorName`, `actorId`, and `workItemId`
  from the first assignment.
- Update the no-runtime-node pending fallback to attach all assignments to the
  first unreached step.
- Update `useRecordApproval` to carry the full pending assignment list into the
  banner projection.
- Update `ApprovalBanner` to select the current user's assignment before
  rendering approve, reject, and reassign controls.
- Update `ApprovalHistory` to display all pending approver names and to use the
  current user's assignment for inline actions.
- Keep the assignment-selection logic in a small shared frontend helper so the
  banner, history, and tests use the same rule.

Backend:

- Add a unit test in the automation package for
  `resolveApproverIDsForNodeWithContext` where the runtime `ProcessNode`
  developer name does not match metadata, but the node label/name does. The
  expected result is the full configured approver list from metadata, proving
  the step-name fallback path is covered.

## Edge Cases

- Empty pending work-item query: the chain still renders the pending step with
  no action controls, matching the existing fail-closed behavior.
- Duplicate approver IDs: backend resolution already de-duplicates metadata
  approvers; the frontend still treats work items as the source of truth and
  avoids action ambiguity by matching the first assignment for the current user.
- Reassignment: after reassignment, the refreshed chain reads the new actor from
  the work item query, so no local mutation path is needed.
- Long approver lists: the UI should show a compact joined display name rather
  than expanding layout-critical controls per approver.
- Salesforce mode and Nexus backend mode use the same chain builder, so the
  frontend fix applies to both data sources.

## Verification

- Frontend unit tests cover:
  - one pending runtime node with multiple work items keeps all assignments
  - single-approver compatibility fields remain populated
  - no-runtime-node fallback carries all pending assignments
  - banner/action selection picks the current user's work item instead of the
    first work item
  - banner projection carries all assignments from the chain into
    `RecordApprovalStatus`
- Backend unit tests cover metadata step-name fallback returning multiple
  approver IDs.
- Focused commands:
  - `cd frontend && npm run test -- approvalChainBuilder approvalPendingAssignments useRecordApproval`
  - `cd backend && go test ./internal/features/automation`
- Baseline commands after implementation:
  - `cd backend && go test ./...`
  - `cd shared && go test ./...`
  - `cd frontend && npm run build`
  - `git diff --check`

## Design Iteration Notes

1. The first obvious implementation would duplicate the pending chain step once
   per approver. That makes action rendering easy, but it misrepresents one
   runtime `ProcessInstanceNode` as many approval steps and breaks the chain
   progress model.
2. The second option is to add a new backend endpoint that returns a custom
   record-detail approval projection. That would introduce a parallel source of
   truth even though SOQL already exposes the canonical Salesforce-modeled
   `ProcessInstanceWorkitem` rows.
3. The selected design keeps the chain step as the runtime node and extends it
   with pending assignments. It reuses existing `ProcessInstanceWorkitem`
   records, preserves single-approver fields, and lets every action call the
   existing approve/reject/reassign APIs with the correct work item ID.
4. The final pre-implementation code scan checked every current
   `ApprovalChainStep` consumer. Only `useRecordApproval`,
   `ApprovalBanner`, and `ApprovalHistory` make action/display decisions from
   pending actor fields, so the implementation can stay narrow.

References:

- Issue #748: https://github.com/pingcap-inc/NexusCRM/issues/748
