> ## Documentation Index
> Fetch the complete documentation index at: https://help.stralos.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Day rate inheritance review frontend

> Base review dialog, rebase bucket display, and duplicate-for-client flow in the day rate schedules panel

This page covers the M3 inheritance review UI in `stralos-feature-rates`. The relevant components are `DayRateInheritance.tsx` (dialogs) and `DayRatesPanel.tsx` (schedule list and orchestration).

## Day rate schedule list

`DayRatesPanel` lists active day rate schedules at the organisation level by default. **Show archived schedules** switches the list to archived schedules, which are read-only until restored. Each row in the table shows:

* **Schedule** — the schedule name.
* **Client** — derived from the displayed revision's `clientNameSnapshot`; falls back to `"Internal"` when no client is set and `"Client unavailable"` when `clientIdSnapshot` is set but the snapshot is null.
* **Derivation** — computed by `selectedDerivation()` from the current or displayed revision. Resolves the base revision name and adjustment percentage from the in-memory revision index. Falls back to `"Inherited ± N% · pinned revision"` when the base revision is not in the list; `"Client-defined lines"` when `lineAuthoritySnapshot === "client_defined"`; `"Independent · from <date>"` otherwise.
* **Status** — current and draft revision buttons, plus one orange **Review required · Rev N** button per non-superseded revision with `needsBaseReview: true`.
* **Actions** — a **Duplicate for client** button, shown only when `canEdit` is true and at least one published revision exists.

Admins see full edit controls. Members see a **Read only** label and no publish, draft-cut, or duplicate actions.

Archived rows can still show revision and review status, but the panel passes `canEdit={false}` for them. An admin must restore a schedule before acknowledging or applying an inheritance review.

### Review chips

`reviewRevisions()` filters to revisions where `status !== "superseded" && needsBaseReview`. Each result produces an orange **Review required · Rev N** button that opens the review dialog for that specific revision. The chip disappears from the list after a successful `acknowledgeDayRateBase` call clears `needsBaseReview` on that revision. Other revisions on the same schedule keep their own chips if they still require review.

### Schedule-level `needsBaseReview`

`withUpdatedRevision()` recomputes `schedule.needsBaseReview` on any local update as `revisions.some((r) => r.status !== "superseded" && r.needsBaseReview)`. The panel does not re-fetch the full list after a review action; it applies the updated revision in place.

## Rebase review dialog

`RebaseReviewDialog` opens when an admin clicks **Review required · Rev N** in the schedule list, then **Review base changes** in the expanded revision detail. Both buttons refer to the same revision.

### Preview loading

On mount, the dialog calls `previewDayRateRebase(scheduleId)` only when the displayed revision matches `schedule.currentRevision` (`canPreviewDisplayed`). A displayed revision that is not the current one cannot generate a fresh preview and shows a read-only message; it can still be acknowledged with **Keep pinned**.

Preview loading errors display an inline alert and a **Retry review** button. The dialog does not suppress dismiss during a load — only during a submit.

### Rebase buckets

`DayRateRebasePreview.buckets` is keyed by `DayRateRebaseBucket`. The order is defined by `DAY_RATE_REBASE_BUCKETS` (imported from the domain package). `RebaseBucketTable` renders each bucket in that order.

| Bucket | Label | Behaviour |
| - | - | - |
| `added` | Added by the base | New lines the base introduced. `oldBaseDayRate` is absent; only `newBaseDayRate` and `resultDayRate` are shown. |
| `updated` | Updated by the base | Unmodified inherited lines that adopt base changes. |
| `retainedOverrides` | Retained overrides | Lines with negotiated rate overrides. Base fields other than the rate update; the override rate stays. |
| `removed` | Removed by the base | Lines the base has removed. Each row has a checkbox so the admin can select which removals to apply. Unselected lines become independent local lines. |
| `suppressed` | Suppressed base lines | Lines the admin previously removed. They stay removed. |
| `local` | Local lines | Lines created directly on this schedule. Untouched. |
| `unchanged` | Unchanged | Lines where the lineage refreshes to the new base revision with no commercial change. |

`removed` is the only bucket with checkboxes. The checkbox state is held in a `Set<string>` of `lineKey` values. The set is reset to empty whenever the preview refreshes.

### Pricing-rule buckets

`DayRateRebasePreview.ruleBuckets` separates pricing-rule outcomes from line outcomes. `DAY_RATE_RULE_REBASE_BUCKETS` supplies their display order.

| Bucket | Label | Behaviour |
| - | - | - |
| `adopted` | Rule changes adopted | Base-only changes flow into the draft. |
| `retainedOverrides` | Rule overrides retained | Locally edited or deleted rules remain contractual while the new base value stays visible for comparison. |
| `local` | Local rules kept | Rules without a copied baseline remain local. |
| `removed` | Base rules removed | Unmodified rules removed from the base leave the draft. |
| `unchanged` | Rules unchanged | The rule follows the newer base revision with no commercial change. |

Each row identifies whether the rule applies to the whole schedule or a line, its slot and condition, and the old base, new base, current, and draft-result forms. Rule removals are outcomes rather than selectable choices; the checkboxes apply only to removed lines.

### Rate formatting

`formatRate(value, rateKind, present)` renders currency as NZD with two decimal places using `Intl.NumberFormat("en-NZ")`. When `rateKind === "cost_plus"` it returns `"Cost plus"` regardless of value. When `present` is false (the value is absent for this bucket, e.g. the old base rate on an `added` line) it returns an em dash (`—`).

### Keep pinned

**Keep pinned** calls `acknowledgeDayRateBase(scheduleId, displayedRevisionId, acknowledgementTargetId)`.

`acknowledgementTargetId` resolves as follows:

* When the displayed revision is the current revision and a preview loaded successfully, it is `preview.targetBaseRevisionId`.
* Otherwise it is `reviewingRevision.baseRevisionId` resolved through the revision index to the base schedule's current revision id (`revisionIndex.get(baseRevisionId)?.schedule.currentRevision?.id ?? null`).

When `acknowledgementTargetId` is null — for example because the base schedule is not in the loaded list — the **Keep pinned** button is disabled and an explanatory `role="status"` message is shown.

### Create draft revision

**Create draft revision** calls `applyDayRateRebase(scheduleId, preview.targetBaseRevisionId, [...removals].sort())`. Removal keys are sorted before sending so the payload is deterministic.

The button is disabled with a descriptive reason when:

* A draft revision already exists on this schedule.
* The displayed revision is not the current revision.
* The user is not an organisation admin (`!canEdit`).

The reason string is shown inline as a `role="status"` message above the bucket tables.

### Stale 409 recovery

When `applyDayRateRebase` returns a 409, the dialog attempts recovery:

1. Calls `onRefreshSchedule()` to reload the schedule detail.
2. Calls `previewDayRateRebase` again to fetch a fresh preview.
3. Resets `removals` to an empty set.
4. Displays an inline error telling the admin the review was refreshed and all outcomes must be checked again.

If the refresh also fails, the preview is cleared and the generic stale-error message is shown.

Other API errors (404, 422, and unexpected) are surfaced using `safeApiMessage()`, which maps:

| Status pattern | User message |
| - | - |
| `status 404` | "This schedule or client is unavailable. Refresh and try again." |
| `status 409` | "The schedule changed or conflicts with an existing schedule. Refresh or choose different values and try again." |
| `status 422` | "Some submitted values are no longer valid. Review them and try again." |
| Other | The error's own message string. |

## Duplicate for client dialog

`DuplicateDayRateScheduleDialog` opens when an admin clicks **Duplicate for client** on a schedule row. The source schedule's published revisions are available as source options.

### Fields

| Field | Validation |
| - | - |
| Target schedule name | Required. 200 characters maximum. |
| Client | Optional. Must match a client returned by `listAllClients()`. Empty selects "Internal / no client". |
| Published source revision | Required. Must be a published (non-draft) revision of the source schedule. |
| Derivation | Radio: **Copy** (independent, no inheritance pin) or **Inherit** (pinned to the source revision). |
| Rate adjustment (%) | Required. Must be a number with no more than three decimal places, greater than −100 and no more than +1000. Applies to flat and fixed rates only; cost-plus rules and minimums or loadings are copied unchanged. |

### Client loading

`listAllClients()` is called on mount. Despite the compatibility name, it pages the Companies register through `/api/global/parties?role=client` and projects each party to the `ClientSummary` shape required by the picker. While loading, the **Create duplicate** button is disabled. A load error shows an inline alert with a **Retry clients** button. The client picker is disabled during load or when a load error exists.

### Submission

`duplicateDayRateSchedule(sourceScheduleId, input)` is called on valid submit. On success, `onCreated(schedule)` is called with the new schedule. `DayRatesPanel` prepends the new schedule to the list, expands it, and selects its draft revision.

The form disables double-submit via a `busyRef`. API errors are surfaced via `safeApiMessage()`.

A dirty-close guard prompts the admin before closing the dialog when any field has changed from its default.

## API operations

| Operation | Description |
| - | - |
| `previewDayRateRebase(scheduleId)` | Returns `DayRateRebasePreview` with `targetBaseRevisionId`, line `buckets`, and pricing-rule `ruleBuckets`. |
| `acknowledgeDayRateBase(scheduleId, revisionId, targetBaseRevisionId)` | Records that the admin has reviewed the base change and is keeping this revision pinned. Returns an updated `DayRateRevision` with `needsBaseReview: false` and `baseAckRevisionId` set. |
| `applyDayRateRebase(scheduleId, targetBaseRevisionId, removalLineKeys)` | Creates a rebased draft revision incorporating the reviewed changes. Returns `DayRateRebaseResult` containing the new `revision`. |
| `duplicateDayRateSchedule(sourceScheduleId, input)` | Creates a new schedule from the source revision with optional inheritance pin and adjustment. Returns the created `DayRateSchedule`. |
| `listAllClients()` | Returns client-role Companies register parties as `ClientSummary[]`. Used to populate the duplicate dialog's client picker. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.