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 renderImportWizardSteps from ImportWizardSteps.tsx using the DAY_RATE_WIZARD_STEPS constant. The step list is ["parse", "review"], which renders as two labelled steps:
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 anoutline 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:
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.
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:
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.
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:
DayRateImportLine carries:
confidence below 0.85 causes a Needs review badge on the line in the review table.
Review table rendering
The review table is aTable.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:
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.
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 callscommitDayRateScheduleImport(input), which posts to POST /api/rates/day-rate-schedules/import/commit. The payload includes:
Error messages
safeImportMessage() maps HTTP status codes from error messages to user-facing strings:
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 for the wire contract and Rate import service for backend behaviour.
