Skip to main content
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. 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. 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:

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

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