> ## 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 import frontend

> Frontend contract for the Schedule of Rates spreadsheet import flow in stralos-feature-rates

The day rate import flow lives in `stralos-feature-rates`. `DayRateImportPage` is registered at `/rates/day-rates/import` and is available only to organisation admins. Non-admins see an access-required state instead of the upload controls.

The `DayRatesPanel` component exposes an **Import Schedule of Rates** button that navigates to this route. The button is shown only when the active schedules view is displayed — it is hidden while the archived view is active.

## Step row

Both screens in the import render `ImportWizardSteps` from `ImportWizardSteps.tsx` using the `DAY_RATE_WIZARD_STEPS` constant. The step list is `["parse", "review"]`, which renders as two labelled steps:

| Step key | Label |
| - | - |
| `parse` | Upload |
| `review` | Check and import |

`DAY_RATE_WIZARD_STEPS` is a named list rather than a fourth `ImportPath` because the rate importer's `importWizardOverviewSlots` function enumerates paths to build the **How importing works** overview card on the upload screen. Adding a day-rate path there would put an unrelated card on the rate importer's screen. Both steps share the same label strings as the equivalent steps on the rate importer paths via `STEP_LABELS`.

The upload screen passes `current="parse"` and the review screen passes `current="review"`. The row appears inside `PageHeader` on each screen, below the title — matching the position the rate importer uses on its screens.

## Cancel button on the review screen

The review screen footer includes a **Cancel** button (rendered as an `outline` button) alongside the **Import as draft** primary. Clicking it navigates back to `?view=day-rates` without completing the import.

## Workbook preparation

`dayRateSpreadsheetImport.ts` loads the `xlsx` package lazily when a user drops or selects a file. It accepts `.csv`, `.xls`, and `.xlsx` files and applies these limits before calling the API:

| Limit | Value |
| - | - |
| File size | 5 MB |
| File name length | 300 characters |
| Sheets | 20 |
| Sheet name length | 120 characters |
| Rows per sheet | 2,000 |
| Columns per row | 40 |
| Characters per cell | 300 |
| Total cell characters | 450,000 |
| Warning messages | 20 |

Columns past the first 40 in a row are omitted. Each cell value is shortened to 300 characters. Excess rows, sheets beyond the first 20, and total cell content beyond 450,000 characters produce warnings and are omitted. Empty sheets are not sent. Unsupported, empty, oversized, or unpopulated workbooks fail locally with a user-facing message.

### Formula detection and array formula handling

SheetJS (`xlsx`) does not calculate formulas. `format_cell` reads the authored or cached display value; the formula string is carried separately so `api-rates` can identify and omit formula rows from the review.

A cell is treated as formula evidence when either of the following properties is present:

* `cell.f` — the formula string on the anchor cell of a regular or array formula.
* `cell.F` — the array formula range string on non-anchor cells in an array formula range.

The `cell.F` property closes a gap where cells that are part of an array formula but are not the anchor cell carry only `F` (without `f`). Without this check, those cached cells had no formula evidence and could appear in the review as authored money values.

`spreadsheetCell()` in `dayRateSpreadsheetImport.ts` combines both checks:

```ts theme={null}
const formulaEvidence = typeof cell.f === "string"
  ? cell.f
  : typeof cell.F === "string"
    ? cell.F
    : null;
const rawFormula = formulaEvidence === null ? null : boundedCellText(formulaEvidence);
return {
  cell: { value: display.value, formula: rawFormula?.value ?? null },
  shortened: display.shortened || Boolean(rawFormula?.shortened),
};
```

Any row containing at least one cell with non-null `formula` is flagged in `formulaRow`. Formula rows are included in the JSON payload sent to the server (so `api-rates` can track their physical position for `sourceRow` coordinates) but `api-rates` omits them from the returned review lines.

### Warnings

After reading the workbook, `readDayRateSpreadsheet` returns structural and formula warnings:

* Structural warnings (sheet count, row count, character limit, shortened cells) are placed first.
* Formula-row warnings follow in row order, capped at 20 total.

The client merges these with any warnings returned by the server (`combinedWarnings`) before displaying them.

## Analyse and review

`DayRateImportPage` calls `parseDayRateScheduleImport(fileName, sheets)`, which posts to `POST /api/rates/day-rate-schedules/import/parse`. The service returns a `DayRateImportReview`:

```ts theme={null}
type DayRateImportReview = {
  fileName: string;
  warnings: string[];
  lines: DayRateImportLine[];
};
```

Each `DayRateImportLine` carries:

```ts theme={null}
type DayRateImportLine = {
  reviewId: string;         // UUID; unique within the review
  lineCode?: string;        // optional item code from the spreadsheet
  description: string;
  unit: string;
  rateKind: "flat" | "cost_plus" | "fixed";
  dayRate: number | null;   // null for cost_plus lines
  confidence: number;       // 0–1 from the AI analysis
  sourceSheet: string;
  sourceRow: number;        // one-based physical row number
};
```

A `confidence` below 0.85 causes a **Needs review** badge on the line in the review table.

## Review table rendering

The review table is a `Table.Root` with a `VirtualTableBody` from `@stralos-co-nz/ui` 0.65.0. Only the rows currently on screen plus a margin are kept in the DOM; the scroll container and scrollbar span the full imported line list. Virtualisation is transparent to reviewers.

The schedule name field is pre-filled from the file name (stripped of extension, truncated to 200 characters) the first time a file is analysed. Subsequent analyses within the same session do not overwrite a name the user has already entered.

## Client loading

On mount, `DayRateImportPage` calls `listAllClients()` to populate the client picker in the draft identity form. If the client list fails to load, an error banner and a **Retry** button are shown. The client picker is disabled while clients are loading or an error is present.

## Line validation

`lineValidation()` in `DayRateImportPage.tsx` validates each included line before submission:

| Rule | Message |
| - | - |
| Description blank | `"Description is required."` |
| Description over 200 characters | `"Description must be 200 characters or fewer."` |
| Unit blank | `"Unit is required."` |
| Unit over 40 characters | `"Unit must be 40 characters or fewer."` |
| Code over 80 characters | `"Code must be 80 characters or fewer."` |
| `cost_plus` line — no further rate check | (passes) |
| Flat or fixed line — day rate blank | `"A day rate is required for flat and fixed lines."` |
| Day rate non-finite, negative, or over 999,999,999,999.99 | `"Enter a non-negative day rate within the supported money range."` |
| Day rate has more than two decimal places | `"Day rates may have at most two decimal places."` |

`committedLines()` also checks for duplicate codes across included lines and returns the first duplicate as a blocking error.

## Metadata validation

Before submission, `DayRateImportPage` validates the form-level fields:

* **Schedule name** — required.
* **Effective from** — required date. An `Effective from is required.` error is shown if the field is cleared before submitting.

Both produce inline errors on the relevant form fields. Line-level errors are shown beneath the affected row's description cell.

The **Effective from** field uses `DateField` rather than `<Input type="date">`. A native date input renders in the browser's locale, which showed `08/31/2026` on an NZ product. `DateField` displays day-first (`7 Sep 2026`), accepts day-first typed input (`7/9/2026` parses as 7 September), and returns an ISO date (`YYYY-MM-DD`) that is submitted as `effectiveFrom`.

### Default Effective from date

When a workbook is analysed for the first time in the session, `DayRateImportPage` pre-fills **Effective from** with the viewer's local calendar date (`localTodayIsoDate()`). This default is set once only: a subsequent re-analysis (for example, after the reviewer drops a corrected file) does not overwrite a date the user has already cleared or changed. The behaviour mirrors `DayRateScheduleCreateDialog`, which uses the same local-today seed for consistency.

## Commit

On submission, the page calls `commitDayRateScheduleImport(input)`, which posts to `POST /api/rates/day-rate-schedules/import/commit`. The payload includes:

```ts theme={null}
type DayRateImportCommitInput = {
  fileName: string;
  name: string;
  clientId?: string | null;
  contractRef?: string | null;
  effectiveFrom: string;
  termsText?: string | null;
  lines: DayRateImportLine[];
};
```

On success the page shows a toast with the imported line count, then navigates to:

```
/rates?view=day-rates&schedule=<scheduleId>&revision=<revisionId>
```

The result type is:

```ts theme={null}
type DayRateImportResult = {
  scheduleId: string;
  revisionId: string;
  status: "draft";
  importedLineCount: number;
};
```

## Error messages

`safeImportMessage()` maps HTTP status codes from error messages to user-facing strings:

| Status | Message |
| - | - |
| 502 | `"The Schedule of Rates spreadsheet could not be analysed. Check the file and try again."` |
| 409 | `"A schedule with this name already exists. Choose a different name and try again."` |
| 404 | `"The selected client is no longer available. Reload clients and try again."` |
| 403 | `"Organisation admin access is required to import a Schedule of Rates."` |

Other errors surface the raw message from the caught `Error` instance.

## Archive

`DayRatesPanel` exposes an **Archive** button on each schedule row when `canEdit` is true. The archive action calls `deleteDayRateSchedule(schedule.id)` (a soft delete via `DELETE /api/rates/day-rate-schedules/:id`). All revisions, lines, pricing rules, and existing references remain readable after the call.

A browser `confirm` dialogue is shown before the request is issued. If the user cancels, no request is made.

After a successful archive, the schedule is removed from the active list immediately without a full reload.

### Archived view

The panel shows either active or archived schedules, never both. Toggling is done via the **Show archived schedules** / **Show active schedules** button. The **Import Schedule of Rates** button and **Archive** action are hidden in the archived view. Archived schedules and their revisions are read-only — the workspace shows a notice instead of edit controls.

`listAllDayRateSchedules` is called with `"active"` or `"archived"` as the `scheduleStatus` argument depending on which view is active.

## API types

`DayRateScheduleLifecycleStatus` is `"active" | "archived"`. The schedule row carries `scheduleStatus` on `DayRateSchedule`. Archived schedules have `scheduleStatus === "archived"`.

See [Rates API](https://help.stralos.app/api/rates.md) for the wire contract and [Rate import service](https://help.stralos.app/internal/rates-import-service.md) for backend behaviour.


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