Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40) - #52

Merged
corrin merged 20 commits into
mainfrom
timesheet-entry
Aug 10, 2026
Merged

Timesheet entry: the daily/entry surface — 5 specs green (21 of 40)#52
corrin merged 20 commits into
mainfrom
timesheet-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The timesheet-entry slice: the daily overview and entry pages, a sibling grid over the draft machinery extracted from the cost grid, and two read ops. Full E2E gate green on this branch (67/67, writes live); 1731 unit tests; coverage 88.56%.

Backend (two read ops)

  • `job_timesheet_entries_retrieve` — the management day view, homed in `apps/timesheet/api.py` keeping the `job_` operation ID (the `job_workshop_timesheets_` precedent) and superuser auth like its management siblings. Reuses the extracted `day_time_lines` queryset and `job_service.cost_line_data` plus a per-line job-identity overlay (the grid's job is per-row, unlike the job costing grid). Summary adds `entry_count` + `scheduled_hours`. The three dead modern-timesheet siblings were confirmed dead surface and not ported.
  • `accounts_staff_list` — the staff admin list (departed included), shipped ahead of the staff slice because `staff-wage-loading` reads it. Deliberately not `get_displayable_staff`: that filter answers "who can record time on a date".

Frontend

  • `features/shared/`: `useAutosaveField` moved as-is; `useDraftRows` extracted mechanically from `CostLineGrid` (phantom invariant, deferred row-exit commit, in-flight guard, failure badge), parameterised by draft type and commit policy. `CostLineGrid.test.tsx` untouched and green; the three green grid specs are the regression net. TableMeta's global augmentation is now namespaced per grid.
  • `SmartTimesheetTable` — the entry grid: per-row job picker (Popover + hand-rolled list; the Tab-commits-highlight contract fights cmdk), humanised hours (`2h` / `3h 30m`), v1-exact urgent/bill precedence (`applyJobPick`; shop/special wins over urgent and over an explicit override), server-owned pricing (PATCHes carry meta so the one rate pipeline reprices; the echo merge applies pricing outputs), and the full selector contract (`DataTable-row-N` + trailing phantom, `SmartTimesheetTable-*` families, `data-entry-seq`, `.smart-timesheet-table`).
  • Entry page: parallel reference queries (what the performance spec measures), loud unknown-staff error, weekend-skipping date navigation per company defaults, live Daily Breakdown tiles. Daily page: read-only summary with the `StaffRow-name-{staffId}` contract.
  • Deferred with seams (no spec asserts them): StaffDetailModal, MetricsModal, the Current Jobs cards (v1's per-job `getJobSummary` N+1 wave), help dialog, container-level grid shortcuts.

Review findings applied (2-subagent adversarial pass)

  • The wage select no longer mirrors into bill — v1's mirror was unreachable once a job was picked (its override check counted the bill key `setJob` always writes), so mirroring clobbered the urgent 1.5x default and could flip a shop job billable (a 422).
  • An errored background refetch keeps the working entry page instead of unmounting the grid and destroying drafts.
  • Dead `select_related` legs dropped (the comment claiming they served the projection was false); draft discard locks in flight; hour totals humanise; garbage URL dates fail loudly.
  • Recorded as an open decision (pre-existing, not changed here): cost-line writes are plain-authenticated with a caller-chosen staff UUID while the timesheet reads are superuser-gated.

Specs & environment

Five specs ported (`timesheet/` ×4 + `staff/staff-wage-loading`) with two full-suite races fixed en route: the Actuals chip's `'—'` placeholder parsing as 0, and a type-then-Tab arriving with the picker highlight reset (sole-match fallback). New environment prerequisite recorded in rewrite-status: the E2E user must be a superuser (management surface); set in the dev DB.
🤖 Generated with Claude Code

https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve

Summary by CodeRabbit

  • New Features
    • Added daily timesheet overview and staff-specific entry pages.
    • Added editable timesheet grids with job search, keyboard navigation, autosave, approvals, deletions, and live totals.
    • Added superuser tools to view all staff and review daily timesheet details, including departed staff.
    • Added date navigation, wage and billing calculations, urgent-job defaults, and pay-item handling.
  • Bug Fixes
    • Improved draft-row persistence, optimistic updates, retries, and rollback behavior.
  • Documentation
    • Updated timesheet delivery status, specifications, and quality metrics.

corrinand others added 18 commits August 10, 2026 06:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…router
The entry grid reads any staff member's day as CostLine-shaped lines (it
patches them through the cost-line endpoints), so the projection reuses
job_service.cost_line_data plus a job-identity overlay per line; the
queryset is the one list_entries already had, extracted as day_time_lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
All staff including departed (the admin screen and wage-loading checks
read them) — deliberately not get_displayable_staff, which answers who
can record time on a date. Create/patch/icon stay with the staff slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
useAutosaveField moves as-is; useDraftRows lifts the phantom invariant,
deferred row-exit commit, in-flight guard and failure badge out of
CostLineGrid, parameterised by draft type and commit policy so the
timesheet grid can share them. CostLineGrid.test.tsx is untouched and
green — the extraction is behaviour-preserving by that net.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Popover + hand-rolled list rather than cmdk: the keyboard contract is a
maintained highlight with Enter/Tab committing it, which fights cmdk's
internal selection model rather than reusing it. Trigger auto-opens on
focus for an empty row (the create flow's focus handoff lands there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The useCostLines discipline rebound to the entries envelope. Pricing is
server-owned (any meta/labour_subtype PATCH on a created_from_timesheet
line reprices through the one rate pipeline), so the echo merge applies
pricing outputs unconditionally; created lines are enriched with the
picked job's identity because the job router's echo carries none.
restoreDeletedRow extracted to features/shared for both hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
CostLineGrid's sibling: the job is a per-row editable field (the picker),
hours humanise on commit (2h / 3h 30m), the urgent/bill precedence ports
v1's setJob exactly, and pricing edits ship meta so the server rate
pipeline reprices. TableMeta's global augmentation namespaces per grid
(costGrid / timesheetGrid) so both tables can type their own context.
shadcn select added for the rate/billRate/labourType columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Entry page: parallel reference queries (the performance spec measures
exactly this), loud unknown-staff error, weekend-skipping date nav per
company defaults, live Daily Breakdown tiles; Current Jobs cards and
the modals are deferred with seams — no spec asserts them. Daily page:
read-only summary with the StaffRow-name-{staffId} contract the specs
extract the entry staffId from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
create-timesheet-entry (8 cases), keyboard-nav, urgent-job-defaults,
performance, staff-wage-loading, plus shared support helpers, timesheet
API fixtures and getPhantomRowIndex. One backend-contract fix surfaced:
a blank create description now ships as null (desc_not_blank, ADR 0040).
Port deviation: serial-first-test job creation via the standard fixture
replaces v1's hand-rolled beforeAll logins. tests/e2e joins the
no-console lint exemption (the performance spec's output IS its report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Also records the pre-existing cost-line write-auth asymmetry as an open
decision (management reads are superuser-gated; the writes the entry
grid reuses are plain-authenticated with a caller-chosen staff UUID).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
…errors
The wage select now changes only the wage multiplier — v1's mirror was
unreachable once a job was picked (job pick writes the bill key, which
its override check counted as explicit), so mirroring here clobbered the
urgent 1.5x default and could flip a shop job billable (a 422). Also:
an errored background refetch keeps the working entry page instead of
unmounting the grid (destroying drafts), the draft discard button locks
in flight, hour totals humanise (no float-tail renders), and garbage
URL dates fail loudly instead of flowing through as NaN. Two inherited
shapes recorded in the backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
'—' strips to Number('') === 0, so the pre-data render both false-passed
the zero assertion and false-failed the greater-than-zero one under
full-suite load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
The full-suite gate caught a rapid type-then-Tab under load arriving
with the highlight reset: the popover stayed open and no job committed.
With exactly one match there is no ambiguity, so the fallback cannot
misfire; multi-match with no highlight still refuses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@coderabbitai

coderabbitaiBot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@corrin, you've reached your PR review limit, so we couldn't start this review.

Next review available in:36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb72233a-561b-4a4f-a2e6-358e55497309

📥 Commits

Reviewing files that changed from the base of the PR and between c5b5ebb and 2c70d1c.

📒 Files selected for processing (18)
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/lineMeta.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
📝 Walkthrough

Walkthrough

Adds superuser staff and management-timesheet APIs, typed frontend contracts, shared draft and autosave behavior, editable timesheet pages, authenticated routes, and backend, unit, component, and end-to-end tests.

Changes

Timesheet management slice

Layer / File(s)Summary
Backend management APIs
apps/accounts/..., apps/timesheet/...
Adds superuser staff listing and dated timesheet retrieval. Responses include staff identity, wage data, cost lines, job details, and daily summaries.
Frontend contracts and shared primitives
frontend/schema.v2.yml, frontend/src/api/index.ts, frontend/src/features/shared/..., frontend/src/features/job/costing/...
Adds generated API contracts, reusable autosave and draft-row hooks, optimistic row restoration, and costing-grid integration.
Timesheet domain models and data hooks
frontend/src/features/timesheet/hours.ts, frontend/src/features/timesheet/labourRates.ts, frontend/src/features/timesheet/timesheetDraft.ts, frontend/src/features/timesheet/TimesheetJobPicker.tsx, frontend/src/features/timesheet/useTimesheetEntries.ts, frontend/src/lib/dates.ts
Adds hours and date utilities, labour-rate resolution, billing defaults, job selection, and optimistic entry mutations.
Timesheet entry workspace
frontend/src/features/timesheet/SmartTimesheetTable.tsx, frontend/src/features/timesheet/TimesheetEntryPage.tsx, frontend/src/features/timesheet/DailyOverviewPage.tsx, frontend/src/routes/_authed/timesheets/*
Adds editable entry and read-only daily pages with date and staff navigation, summaries, draft rows, autosave, approvals, deletion, and authenticated routing.
Validation and delivery coverage
apps/*/tests/*, frontend/src/features/timesheet/*.test.*, frontend/tests/e2e/timesheet/*, docs/*
Adds API, component, unit, and end-to-end coverage. Updates implementation plans, rewrite status, and code-quality metrics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.59% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the timesheet entry slice and its daily/entry surface, which matches the primary changes.
Description check✅ PassedThe description clearly explains the backend, frontend, testing, environment, deferred work, and review fixes for the timesheet-entry slice.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch timesheet-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
frontend/src/features/shell/AppNavbar.tsx (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a data-automation-id to the Timesheets link.

The Create Job link at Line 36 carries AppNavbar-create-job. This link has no automation id, so specs must select it by visible text. Text selectors break on a label change.

♻️ Proposed refactor
- <Link to="/timesheets/daily" className="text-sm text-gray-700 hover:text-gray-900">+ <Link+ to="/timesheets/daily"+ data-automation-id="AppNavbar-timesheets"+ className="text-sm text-gray-700 hover:text-gray-900"+ >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/shell/AppNavbar.tsx` around lines 42 - 44, Add a stable
data-automation-id attribute to the Timesheets Link in AppNavbar, using the
existing naming convention and a unique identifier such as AppNavbar-timesheets.
Keep the current destination, styling, and visible label unchanged.
frontend/src/features/timesheet/SmartTimesheetTable.tsx (2)

717-725: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The phantom focus effect queries the whole document.

document.querySelectorAll matches picker triggers in any mounted SmartTimesheetTable. If a second grid ever mounts (for example a split staff view), focus lands in the wrong grid. A ref on the root div scopes the query to this instance. The same applies to focusAutomationId at Line 194.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 717 -
725, Scope both picker-trigger lookups to the current SmartTimesheetTable
instance instead of the global document. Add or reuse a ref on the root div,
query through that ref in the focusPhantomToken effect and in focusAutomationId,
and preserve the existing trigger-selection and focus behavior within that
table.

116-122: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a job lookup map instead of a linear scan per cell.

jobForRow runs context.jobs.find(...) for every server row. Five cell components call it (JobPickerCell, CompanyCell, JobNameCell, LabourTypeCell, BillRateCell), so each render costs O(rows × jobs × 5). A Map built once in the meta object removes the scan.

♻️ Proposed refactor
 interface TimesheetCellContext {
jobs: readonly TimesheetJobOut[]
+ jobsById: ReadonlyMap<string, TimesheetJobOut>
payItems: readonly XeroPayItemOut[]
 function jobForRow(
context: TimesheetCellContext,
gridRow: TimesheetGridRow,
): TimesheetJobOut | null {
if (gridRow.type === 'draft') return gridRow.draft.job
- return context.jobs.find((job) => job.id === gridRow.line.job_id) ?? null+ return context.jobsById.get(gridRow.line.job_id) ?? null
}

Build the map with useMemo next to rows and pass it in meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx` around lines 116 -
122, Refactor jobForRow and the surrounding table setup to use a memoized Map of
jobs keyed by job.id, built alongside rows and passed through the meta object.
Replace the context.jobs.find lookup for non-draft rows with a Map lookup while
preserving draft-row handling and the null fallback; update all cell callers to
use the new meta-provided map.
frontend/src/features/timesheet/TimesheetEntryPage.tsx (1)

114-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SmartTimesheetTableProps instead of deriving from Parameters.

SmartTimesheetTable.tsx Line 49 exports SmartTimesheetTableProps. SmartTimesheetTableProps['jobs'] reads more directly than Parameters<typeof SmartTimesheetTable>[0]['jobs'] and survives a signature change to the component.

As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx` around lines 114 -
115, Update the types around the timesheet entry props to import and use the
exported SmartTimesheetTableProps from SmartTimesheetTable.tsx, replacing the
Parameters<typeof SmartTimesheetTable>[0] derivations with
SmartTimesheetTableProps['jobs'] and SmartTimesheetTableProps['payItems']; leave
the underlying component behavior unchanged.

Source: Coding guidelines

frontend/src/features/timesheet/DailyOverviewPage.tsx (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the query options object for the invalidation key.

getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }) is built twice for the same query. Hoist it into a variable and read .queryKey from it. That keeps the key and the query provably identical.

♻️ Proposed refactor
- const summaryQuery = useQuery(- getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } }),- )+ const summaryOptions = getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })+ const summaryQuery = useQuery(summaryOptions)
- void queryClient.invalidateQueries({- queryKey: getDailyTimesheetSummaryByDateOptions({ path: { target_date: date } })- .queryKey,- })+ void queryClient.invalidateQueries({ queryKey: summaryOptions.queryKey })

Also applies to: 71-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx` around lines 30 - 32,
Hoist the result of getDailyTimesheetSummaryByDateOptions({ path: { target_date:
date } }) into a shared query-options variable in the DailyOverviewPage query
flow, pass that variable to useQuery, and use its queryKey for invalidation
instead of constructing the options a second time.
frontend/tests/e2e/staff/staff-wage-loading.spec.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or complete the port-history comments.

These comments narrate the port and prior review context. They do not state the factual constraint that rejects the alternative setup. Keep a comment only when it records both the rejected alternative and that constraint.

  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts#L11-L18: remove the port-history text or state why the standard fixture is required.
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts#L7-L17: remove the port-history text or state the constraint that rejects the alternative setup.
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts#L9-L18: remove the port-history text or state the constraint that rejects the alternative setup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts` around lines 11 - 18,
Remove the port-history and prior-review narration from the comments in
frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.

Source: Coding guidelines

frontend/src/features/timesheet/timesheetDraft.test.ts (1)

95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the test name.

The test picks urgentJob, but the name says "a normal repick". Rename it to match the case under test, for example an explicit override survives an urgent repick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts` around lines 95 -
103, Rename the test case around applyJobPick to describe the urgentJob
scenario, changing “a normal repick” to “an urgent repick” while leaving the
test behavior unchanged.
frontend/src/features/timesheet/TimesheetJobPicker.tsx (1)

210-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the highlighted option to assistive technology.

The list uses plain div elements. The highlight is visual only. A screen-reader user hears no active option while the arrow keys move the highlight. Keyboard operation still works, so this is not a blocker.

Add role="listbox" on the container, role="option" plus aria-selected on each row, stable option ids, and aria-activedescendant plus aria-controls on the search input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx` around lines 210 -
250, Update the TimesheetJobPicker list and keyboard controls to expose the
highlighted job to assistive technology: add listbox semantics to the results
container, option roles with aria-selected and stable ids to each mapped job
row, and connect the search input using aria-controls and aria-activedescendant
that tracks the highlighted option.
frontend/src/features/timesheet/useTimesheetEntries.test.tsx (1)

199-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the toast, or drop it from the test name.

The name says "and toasts", but the test only checks the rollback. Add an assertion on the sonner toast, or rename the test to describe the rollback only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx` around lines
199 - 210, Update the test named “a failed patch rolls back only its own fields
and toasts” to assert the expected sonner toast after the failed patch,
preserving its rollback assertion; alternatively, rename the test to describe
rollback only if toast behavior is intentionally out of scope.
frontend/src/lib/dates.ts (1)

10-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject out-of-range date components.

parseLocal accepts any numeric components. Date normalises them, so 2026-13-45 becomes 2027-02-14 without an error. The date arrives from a user-editable URL search param, so a silently shifted date is possible. Validate the round-trip instead.

♻️ Proposed refactor
 ) {
throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)
}
- return new Date(year, month - 1, day)+ const date = new Date(year, month - 1, day)+ // Date normalises out-of-range parts (month 13 → next January), so the+ // round-trip is the only check that rejects them.+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {+ throw new Error(`Not a YYYY-MM-DD date: ${isoDate}`)+ }+ return date
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/lib/dates.ts` around lines 10 - 25, Update parseLocal to reject
out-of-range date components by validating the constructed Date round-trips to
the original year, month, and day. Throw the existing invalid-date error when
the normalized Date values do not match the parsed components, while preserving
current handling of non-numeric input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/code-quality.md`:
- Around line 101-103: Update the optional-return metric in
docs/rewrite-status.md to match the measured report: 154 functions returning X |
None out of 1,712 non-test functions. Change only the corresponding
planning/status figures and preserve the document’s existing formatting.
In `@docs/rewrite-status.md`:
- Around line 1080-1087: When the draft job is changed in the job-repick flow,
clear its labour_subtype before the updated draft is rendered so rateForSubtype
cannot use a stale value. Add a regression test covering selection of two jobs
with different labour-rate sets and verify the repicked draft has no labour
subtype.
- Around line 171-184: Enforce authorization for every cost-line write: update
the create operation job_jobs_cost_sets_actual_cost_lines_create and the
cost-line PATCH and DELETE handlers to require office/superuser access or verify
ownership of the referenced staff member before mutating data. Add authorization
tests covering denied and permitted create, PATCH, and DELETE requests.
In `@docs/superpowers/plans/2026-08-10-timesheet-entry.md`:
- Around line 41-166: Run Ruff formatting on the affected plan document to apply
its required formatting changes, then verify the working tree contains only the
formatter’s updates and the CI formatting check passes.
- Around line 386-398: Add the `text` language identifier to the opening fenced
code block containing the timesheet behavior checklist, while leaving its
contents and closing fence unchanged.
In `@docs/superpowers/specs/2026-08-10-timesheet-entry-design.md`:
- Around line 3-6: Correct the future-dated status records: in
docs/superpowers/specs/2026-08-10-timesheet-entry-design.md lines 3-6, replace
the future approval and completed green-result wording with the actual approval
date or planned status; in docs/rewrite-status.md lines 16-22, replace the
future last-updated and landed-slice wording with the actual update date or
planned status.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 124-137: The staff-name trigger in the DailyOverviewPage row
currently uses a clickable div, so replace that div with a button that preserves
the existing onOpen behavior, automation identifier, and visual styling while
presenting the name as text. Ensure the button retains the flex layout and
cursor appearance without adding a separate key handler, and keep its child
initials, name, and entry-count content unchanged.
In `@frontend/src/features/timesheet/hours.ts`:
- Around line 14-36: Update parseHoursInput to recognize the humanized Nh Mm,
Nh, and Mm forms emitted by formatHoursDisplay before the generic numeric
parsing path, converting hours and minutes to a decimal hour value so displayed
values such as “3h 30m” round-trip without losing minutes. Preserve the existing
fraction parsing, validation, fallback, and capping behavior.
In `@frontend/src/features/timesheet/SmartTimesheetTable.test.tsx`:
- Around line 240-265: Update the test case around the existing user instance so
pickJob receives user instead of creating a second userEvent.setup() instance.
Keep all subsequent keyboard interactions and focus assertions unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 288-291: The billableFlag reader duplicates lineIsBillable and can
diverge from the grid logic. Move the shared is_billable reader and true
fallback into one exported utility in features/timesheet/, then update
billableFlag and SmartTimesheetTable’s lineIsBillable usage to import that
implementation and remove the duplicate definitions.
In `@frontend/src/features/timesheet/TimesheetJobPicker.test.tsx`:
- Around line 132-135: Update the “never opens when disabled” test to focus the
picker trigger after rendering with disabled enabled, then assert the search
input is absent. Keep the existing disabled assertion and target the trigger and
search-input helpers already used by the test suite.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 46-49: Update jobDisplayRate to handle an empty job.labour_rates
array by returning the picker’s existing placeholder value for that row before
calling rateForSubtype; preserve rateForSubtype’s throwing behavior for
non-empty rates with genuinely unknown subtypes.
- Around line 170-174: Update the picker state around the trigger’s onFocus
handler and onCloseAutoFocus to track Escape or outside-click dismissal, and
skip auto-opening once when focus is restored to an empty enabled row. Clear the
dismissal marker on normal create-flow focus or subsequent intentional focus so
the existing auto-open behavior remains intact.
In `@frontend/src/features/timesheet/useTimesheetEntries.ts`:
- Around line 118-127: Update createLine to call cancelInFlight() before
createMutation.mutate, matching patchLine and deleteLine, so in-flight reads
cannot overwrite the newly inserted row before the settled invalidation refetch.
In `@frontend/src/routes/_authed/timesheets/daily.tsx`:
- Around line 6-8: Add a shared YYYY-MM-DD date validator in
frontend/src/lib/dates.ts that returns the input only for matching values and
undefined otherwise. Update validateSearch in
frontend/src/routes/_authed/timesheets/daily.tsx (lines 6-8) and
frontend/src/routes/_authed/timesheets/entry.tsx (lines 6-9) to use it for
search.date, preserving the existing staffId handling in entry.tsx so invalid
dates fall back to localIsoDate().
In `@frontend/tests/e2e/timesheet/performance.spec.ts`:
- Around line 83-84: Update the request-timing probe around page.goto and the
.smart-timesheet-table visibility wait to also wait for all .animate-spin
elements to disappear before classifying requests, matching the first probe’s
loading-completion behavior.
In `@frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts`:
- Around line 111-115: Update the response validation around responseBody so the
test fails when meta is absent or malformed instead of skipping assertions.
Assert that responseBody and responseBody.meta are records before checking
wage_rate_multiplier and bill_rate_multiplier, preserving the expected values of
1.0 and 1.5.
---
Nitpick comments:
In `@frontend/src/features/shell/AppNavbar.tsx`:
- Around line 42-44: Add a stable data-automation-id attribute to the Timesheets
Link in AppNavbar, using the existing naming convention and a unique identifier
such as AppNavbar-timesheets. Keep the current destination, styling, and visible
label unchanged.
In `@frontend/src/features/timesheet/DailyOverviewPage.tsx`:
- Around line 30-32: Hoist the result of getDailyTimesheetSummaryByDateOptions({
path: { target_date: date } }) into a shared query-options variable in the
DailyOverviewPage query flow, pass that variable to useQuery, and use its
queryKey for invalidation instead of constructing the options a second time.
In `@frontend/src/features/timesheet/SmartTimesheetTable.tsx`:
- Around line 717-725: Scope both picker-trigger lookups to the current
SmartTimesheetTable instance instead of the global document. Add or reuse a ref
on the root div, query through that ref in the focusPhantomToken effect and in
focusAutomationId, and preserve the existing trigger-selection and focus
behavior within that table.
- Around line 116-122: Refactor jobForRow and the surrounding table setup to use
a memoized Map of jobs keyed by job.id, built alongside rows and passed through
the meta object. Replace the context.jobs.find lookup for non-draft rows with a
Map lookup while preserving draft-row handling and the null fallback; update all
cell callers to use the new meta-provided map.
In `@frontend/src/features/timesheet/timesheetDraft.test.ts`:
- Around line 95-103: Rename the test case around applyJobPick to describe the
urgentJob scenario, changing “a normal repick” to “an urgent repick” while
leaving the test behavior unchanged.
In `@frontend/src/features/timesheet/TimesheetEntryPage.tsx`:
- Around line 114-115: Update the types around the timesheet entry props to
import and use the exported SmartTimesheetTableProps from
SmartTimesheetTable.tsx, replacing the Parameters<typeof SmartTimesheetTable>[0]
derivations with SmartTimesheetTableProps['jobs'] and
SmartTimesheetTableProps['payItems']; leave the underlying component behavior
unchanged.
In `@frontend/src/features/timesheet/TimesheetJobPicker.tsx`:
- Around line 210-250: Update the TimesheetJobPicker list and keyboard controls
to expose the highlighted job to assistive technology: add listbox semantics to
the results container, option roles with aria-selected and stable ids to each
mapped job row, and connect the search input using aria-controls and
aria-activedescendant that tracks the highlighted option.
In `@frontend/src/features/timesheet/useTimesheetEntries.test.tsx`:
- Around line 199-210: Update the test named “a failed patch rolls back only its
own fields and toasts” to assert the expected sonner toast after the failed
patch, preserving its rollback assertion; alternatively, rename the test to
describe rollback only if toast behavior is intentionally out of scope.
In `@frontend/src/lib/dates.ts`:
- Around line 10-25: Update parseLocal to reject out-of-range date components by
validating the constructed Date round-trips to the original year, month, and
day. Throw the existing invalid-date error when the normalized Date values do
not match the parsed components, while preserving current handling of
non-numeric input.
In `@frontend/tests/e2e/staff/staff-wage-loading.spec.ts`:
- Around line 11-18: Remove the port-history and prior-review narration from the
comments in frontend/tests/e2e/staff/staff-wage-loading.spec.ts lines 11-18,
frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts lines 7-17, and
frontend/tests/e2e/timesheet/keyboard-nav.spec.ts lines 9-18, or replace it with
the concrete constraint that requires the standard fixture/setup; retain only
factual behavioral or setup requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03579f8a-e6cb-422e-aa4f-1a0b34bafa10

📥 Commits

Reviewing files that changed from the base of the PR and between 9bac5e9 and c5b5ebb.

⛔ Files ignored due to path filters (5)
  • frontend/src/api/generated/@tanstack/react-query.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/index.ts is excluded by !**/generated/**
  • frontend/src/api/generated/sdk.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/types.gen.ts is excluded by !**/generated/**
  • frontend/src/api/generated/zod.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (52)
  • apps/accounts/api.py
  • apps/accounts/schemas.py
  • apps/accounts/staff_directory.py
  • apps/accounts/tests/test_staff_list_api.py
  • apps/timesheet/api.py
  • apps/timesheet/schemas.py
  • apps/timesheet/services/workshop_timesheet_service.py
  • apps/timesheet/tests/test_timesheet_entries_api.py
  • docs/code-quality.md
  • docs/rewrite-status.md
  • docs/superpowers/plans/2026-08-10-timesheet-entry.md
  • docs/superpowers/specs/2026-08-10-timesheet-entry-design.md
  • frontend/.oxlintrc.json
  • frontend/schema.v2.yml
  • frontend/src/api/index.ts
  • frontend/src/components/ui/select.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/useCostLines.ts
  • frontend/src/features/shared/optimistic.ts
  • frontend/src/features/shared/useAutosaveField.ts
  • frontend/src/features/shared/useDraftRows.test.tsx
  • frontend/src/features/shared/useDraftRows.ts
  • frontend/src/features/shell/AppNavbar.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.test.tsx
  • frontend/src/features/timesheet/DailyOverviewPage.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.test.tsx
  • frontend/src/features/timesheet/SmartTimesheetTable.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.test.tsx
  • frontend/src/features/timesheet/TimesheetEntryPage.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.test.tsx
  • frontend/src/features/timesheet/TimesheetJobPicker.tsx
  • frontend/src/features/timesheet/hours.test.ts
  • frontend/src/features/timesheet/hours.ts
  • frontend/src/features/timesheet/index.ts
  • frontend/src/features/timesheet/labourRates.ts
  • frontend/src/features/timesheet/timesheetDraft.test.ts
  • frontend/src/features/timesheet/timesheetDraft.ts
  • frontend/src/features/timesheet/useTimesheetEntries.test.tsx
  • frontend/src/features/timesheet/useTimesheetEntries.ts
  • frontend/src/lib/dates.test.ts
  • frontend/src/lib/dates.ts
  • frontend/src/routeTree.gen.ts
  • frontend/src/routes/_authed/timesheets/daily.tsx
  • frontend/src/routes/_authed/timesheets/entry.tsx
  • frontend/tests/e2e/fixtures/api.ts
  • frontend/tests/e2e/helpers.ts
  • frontend/tests/e2e/staff/staff-wage-loading.spec.ts
  • frontend/tests/e2e/timesheet/create-timesheet-entry.spec.ts
  • frontend/tests/e2e/timesheet/keyboard-nav.spec.ts
  • frontend/tests/e2e/timesheet/performance.spec.ts
  • frontend/tests/e2e/timesheet/support.ts
  • frontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts

Comment threaddocs/code-quality.md
Comment on lines +171 to +184
0. **Cost-line write auth is looser than the timesheet reads (found in the
timesheet-entry slice review; predates it).** The management reads
(`/api/timesheets/*`, `/api/job/timesheet/entries/`, `/api/accounts/staff/`)
are superuser-only because they expose wage data — but the write path the
entry grid (and the cost-entry slice before it) uses is plain
authenticated: `job_jobs_cost_sets_actual_cost_lines_create` accepts an
arbitrary `staff` UUID with no ownership check, and cost-line PATCH/DELETE
are likewise open, so any authenticated staff member can attribute, edit
or delete a colleague's time line — bypassing the ownership rule the
self-service workshop endpoints enforce. `job_jobs_cost_sets_retrieve`
also serves every time line's wage-loaded `unit_cost` to any staff.
Tightening mid-week risks the workshop flows, so nothing changed in the
slice; your call whether cost-line writes gate on office/superuser (or
ownership) before or after cutover.

@coderabbitaicoderabbitaiBotAug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization on every cost-line write.

Any authenticated user can create, modify, or delete another staff member’s time line. This bypasses the superuser protection on wage-bearing timesheet reads.

Apply an office/superuser or ownership check to create, PATCH, and DELETE before release. Add authorization tests for each operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/rewrite-status.md` around lines 171 - 184, Enforce authorization for
every cost-line write: update the create operation
job_jobs_cost_sets_actual_cost_lines_create and the cost-line PATCH and DELETE
handlers to require office/superuser access or verify ownership of the
referenced staff member before mutating data. Add authorization tests covering
denied and permitted create, PATCH, and DELETE requests.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately not changed in this PR, and now recorded as open decision 0 in docs/rewrite-status.md: the asymmetry predates this slice (the cost-entry slice already shipped on these write endpoints), and tightening cost-line write auth five days before cutover risks the workshop flows that legitimately use them. The decision — office/superuser gate vs ownership check, before or after cutover — is called out for the maintainer with your exact failure scenario.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corrin, acknowledged. The finding remains valid, but Open decision 0 now records the risk, the affected operations, and the required authorization-model decision.

I will leave this review finding unresolved because the write authorization has not changed. Would you like me to open a follow-up GitHub issue for the office/superuser-versus-ownership decision and its implementation after cutover?

🐇

You are interacting with an AI system.

Comment threaddocs/rewrite-status.md
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threaddocs/superpowers/plans/2026-08-10-timesheet-entry.md Outdated
Comment threadfrontend/src/features/timesheet/TimesheetJobPicker.tsx
Comment threadfrontend/src/features/timesheet/useTimesheetEntries.ts
Comment threadfrontend/src/routes/_authed/timesheets/daily.tsx
Comment threadfrontend/tests/e2e/timesheet/performance.spec.ts
Comment threadfrontend/tests/e2e/timesheet/urgent-job-defaults.spec.ts
Picker: pointer opens belong to the Radix toggle (focus auto-open is
keyboard/handoff-only — the old shape relied on a close-refocus-reopen
flicker and made Escape reopen the row); stale labour_subtype clears on
job repick (a rate the new job lacks would throw in the bill cell);
parseHoursInput round-trips the humanised display ('3h 30m' no longer
silently drops the minutes); one lineMeta module feeds both the grid
and the page tiles; create insert cancels in-flight reads like its
siblings; routes validate the date search param shape (garbage falls
back to today); the daily staff name is a real button; test/format
nits; the optional-returns figure in rewrite-status now points at the
generated report instead of restating it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J334H8wZj3ccTqHZQh7Vve
@corrin
corrin merged commit 3dd2a30 into mainAug 10, 2026
3 checks passed
Sign up for freeto 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

@corrin