Skip to content

Optional intermediate approval step gating the final Approve#12

Open
Ramon-Jimenez wants to merge 1 commit intomasterfrom
feature/intermediate-approval
Open

Optional intermediate approval step gating the final Approve#12
Ramon-Jimenez wants to merge 1 commit intomasterfrom
feature/intermediate-approval

Conversation

@Ramon-Jimenez
Copy link
Copy Markdown
Contributor

Summary

Add an opt-in intermediate approval step per dataset. When enabled, a dataset requires an "Intermediate Approve" action to land before the existing final Approve action becomes available. When the flag is off, nothing changes — all existing workflows behave exactly as before.

Recorded decisions (all confirmed with the requester before implementation):

  • Storage: new intermediateApprovalRequired: boolean and intermediateApproveCode: string fields on DataSetConfiguration. The code points at a Yes/No data element in the approval (APVD) dataset. The new actions write "true" / "false" to that data element with the default COC per (orgUnit, period), matching the existing Submit flow.
  • Read path: one extra /dataValueSets call per item is joined in GetMalDataSetsUseCase (SQL views untouched, per the option (b) we agreed on).
  • Effective state is derivational: effective = stored Yes AND modificationCount == 0. If the source data is edited after an intermediate approval, the list automatically shows Not approved and gates the final Approve action — no side-effect writes to the server.
  • Approve gating: the final Approve action is hidden whenever any selected row fails the intermediate gate (covers both single-row and partial-multi-select cases).
  • Revoke cascade: running Revoke on a dataset with intermediate approval also resets the stored intermediate flag to false, so the approval chain must be replayed from scratch.

What's in the PR

Configuration (DataSetConfigForm.tsx, DataSetConfiguration.ts)

  • New Intermediate approval required checkbox.
  • New Intermediate Approve DataElement selector (disabled until the flag is on — EntitySelector.tsx gains a small `disabled` prop for that).
  • New Intermediate Approve permission group — listed in `dataSetConfigurationActions`, rendered through the same permission-button loop so administrators can grant it to user groups / individual users. Super-admins bypass as with every other action.
  • `DataSetConfiguration.withDefaults` keeps older stored configs backward-compatible: missing fields default to `false` / `""` and missing permission entries to the empty permission set. Applied in DataSetConfigurationD2Repository.ts.

List column (useDataApprovalListColumns.tsx)

  • New Intermediate approval status column, `hidden: true` by default — the existing gear-icon column selector still controls visibility/persistence. Values: Approved, Not approved, N/A (when the flag is off for that dataset).

View model derivation (DataApprovalViewModel.ts)

  • Takes `dataSetsConfig` so it can read the per-dataset flag.
  • Emits `intermediateApprovalRequired`, `intermediateApproved` (stored) and `effectiveIntermediateApproval` (derived) per row.

Row actions (useActiveDataApprovalActions.tsx, useDataApprovalActions.tsx, DataApprovalList.tsx)

  • Intermediate Approve visible when: flag is on, user has permission, effective status is `notApproved`, row has data and a non-zero modification count. Multi-select supported.
  • Intermediate Unapprove visible when: flag is on, user has permission, stored intermediate is Yes (unchanged by the modification mask), and the final approval has not been granted (you need Revoke first otherwise). Multi-select supported.
  • Existing Approve action hidden when any selected row has effective intermediate status `!= approved`. When the flag is off, `effective == "na"` and Approve behaves as before.

Data layer (MalDataApprovalRepository.ts, MalDataApprovalDefaultRepository.ts, UpdateMalApprovalStatusUseCase.ts, GetMalDataSetsUseCase.ts)

  • `setIntermediateApproval({ dataSets, dataSetConfig, approved })` — POST to `/dataValueSets.json`.
  • `getIntermediateApproval({ item, dataSetConfig })` — GET `/dataValueSets` and filter by data element id.
  • Two new actions on `UpdateMalApprovalStatusUseCase`: `intermediateApprove` / `intermediateUnapprove`. The `revoke` branch now also calls `setIntermediateApproval({ approved: false })` when the dataset has the flag on.
  • `GetMalDataSetsUseCase` joins the stored intermediate value into each item alongside the existing modification count.

Docs (docs/data-approval-user-guide.md)

  • Full user guide for the app (list columns, filters, analytics-running notice, contextual actions, dataset config, permission groups, intermediate approval, DataElementGroups flag). The previous docs PR (Add Data Approval user guide #8) covered the non-intermediate parts; this PR rewrites the same file with everything. Whichever PR merges last will need a trivial rebase.

Test plan

  • Create a dataset config with Intermediate approval required off. List column should read N/A, Approve should behave exactly as before, no new actions visible.
  • Turn the flag on and point Intermediate Approve DataElement at a boolean DE in the APVD dataset. Grant Intermediate Approve to a user group, grant Approve to a different user group. Save the config.
  • As a user in the Intermediate Approve group: verify Intermediate Approve appears, Approve is hidden. Run Intermediate Approve on a row; Intermediate approval status flips to Approved and Approve remains hidden (no permission). Intermediate Unapprove now appears.
  • As a user in the Approve group: verify Approve is visible only on rows where effective intermediate status is Approved.
  • Multi-select a mix of intermediate-approved and not-approved rows; Approve should be hidden.
  • After an intermediate approval, have a data-entry user modify a source value so the modification count becomes > 0. Refresh the list: Intermediate approval status should flip back to Not approved, Approve should disappear, and re-running Intermediate Approve should restore the Approved state.
  • On a row with final Approve granted: run Revoke. Both the approval and the stored intermediate value should reset to No; Intermediate approval status should show Not approved after reload.
  • Load an existing dataset configuration (from before this PR) — it should open in the form with the new flag unchecked, no errors, and no permission gaps.

Known limitations

  • The per-item `/dataValueSets` call adds one extra request per row when the flag is on. That matches the existing per-item diff-fetch pattern and should be fine for the list page sizes the app uses (10–50 rows) but is not batched — worth revisiting if we see perf regressions on busy deployments.
  • The d2-ui-components `TableAction` type does not support a "disabled with tooltip" state; the gate is implemented by hiding the Approve action via `isActive`. We briefly considered a snackbar-on-click fallback to explain why Approve is missing, but chose to keep the behavior simple and document it in the user guide instead.
  • A couple of pre-existing `tsc --noEmit` errors remain in the repo (d2-api module not re-exporting `D2Api` etc.); they exist on master and are unrelated to this PR. `react-scripts build` is unaffected.

🤖 Generated with Claude Code

Datasets can now require an intermediate approval before the final
Approve action becomes available. The new step is fully opt-in per
dataset configuration and leaves existing workflows unchanged when
the flag is off.

Configuration (per dataset):
- New "Intermediate approval required" checkbox on the dataset config
  form.
- New "Intermediate Approve DataElement" selector, disabled until the
  flag is on. Must point at a Yes/No data element in the approval
  (APVD) dataset; its value is written with the default COC per
  orgUnit/period by the new actions.
- New "Intermediate Approve" permission group, listing the users and
  user groups that can run the new actions (super-admins bypass as
  with every other action).

List column:
- New "Intermediate approval status" column, hidden by default — can
  be shown via the gear icon. Renders "Approved", "Not approved", or
  "N/A" when the flag is off for that dataset.
- The displayed status is *derivational*: effective = stored Yes AND
  modificationCount == 0. If the source data is edited after an
  intermediate approval, the column automatically flips back to
  "Not approved" and the final Approve action is gated off — without
  any side-effect writes to the stored data element.

Contextual actions:
- "Intermediate Approve" and "Intermediate Unapprove" appear on rows
  where the flag is on and the user has permission. Both support
  multi-select.
- The existing "Approve" action is hidden when any selected row has
  effective intermediate status != approved. This covers the
  single-row case (hidden) and the multi-select-with-partial case
  (hidden entirely).
- "Revoke" on a dataset with intermediate approval also writes
  "false" to the intermediate data element, so the approval chain
  must be replayed from scratch after a revoke.

Data layer:
- New setIntermediateApproval / getIntermediateApproval repository
  methods. Reads are joined in GetMalDataSetsUseCase per item via
  /dataValueSets (same shape as the existing per-item diff query);
  writes go through UpdateMalApprovalStatusUseCase with two new
  actions (intermediateApprove, intermediateUnapprove).
- DataSetConfiguration gains intermediateApprovalRequired and
  intermediateApproveCode attrs and a new "intermediateApprove"
  action in dataSetConfigurationActions. A withDefaults helper
  keeps older stored configs backward-compatible by falling back to
  empty permissions and an unset flag.

Docs:
- New docs/data-approval-user-guide.md covering the full app and the
  intermediate-approval feature (list column, gating, permission
  group, configuration, Revoke cascade).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant