Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40) - #50

Merged
corrin merged 5 commits into
mainfrom
job/estimate-entry
Aug 9, 2026
Merged

Estimate tab: the grid's second config — create-estimate-entry green (15 of 40)#50
corrin merged 5 commits into
mainfrom
job/estimate-entry

Conversation

@corrin

@corrincorrin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

create-estimate-entry.spec.ts is green — all 8 serial tests, and the full run_e2e.sh gate passed 47/47 across the 15 ported spec files on the final code.

What shipped

  • JobEstimateTab: a thin kind=estimate config of the one CostLineGrid (renders for every pricing methodology — T&M jobs estimate too), lazy-wired.
  • Row-exit draft persistence restored to v1's rule: typed rows POST when focus leaves the whole row, never mid-Tab-chain — eager persist-on-completion swapped the row out from under the focused unit-rev cell, the exact race v1's rule prevented. Item picks still persist immediately.
  • Decimal display trimming: inputs show 3, not the wire's 3.000 — typed values round-trip as typed (the spec asserts string equality), and parseDecimalInput canonicalises the same way so the send-dedupe compares like with like.
  • The spec's Tab chain needs no custom handler: desc → quantity → unit cost → unit rev holds in natural DOM order, unit-tested so it can't silently regress.

Review round (adversarial pair, all applied)

  • Blocker: opening the row's own ItemSelect popover read as a row exit (the portal lives outside the tr in the DOM) — POSTing a complete draft as an adjustment and silently discarding the pick. Fixed with a deferred commit any in-React-tree focus cancels; regression-tested.
  • Draft deletion moved to pointerdown (Safari doesn't focus buttons on click; the null-relatedTarget blur would create the line being deleted — invisible in Chromium-only CI).
  • Labour picks keep a user-authored description (v1's nextLabourDesc rule) and drop stale stock bindings.
  • Ledgered: UI-created estimate/quote time lines store NULL xero_pay_item (traced: load-bearing only for actual lines; job-creation-seeded lines still carry the default — recorded so neither side gets "fixed" unilaterally).
  • Deferred list updated in rewrite-status: summary card + arrow-key/Ctrl+D nav added so no v1-visible feature falls off the cutover list silently.

Spec port deviations (recorded in the file)

Shared job created by the first serial test via the standard fixture; waitForAutosave replaces every sleep, armed before the triggering action; row exit via a heading click; post-pick row finds retry via toPass (v1's sleeps papered over the draft→server row swap); a loud guard for greps of later serial tests.

100 frontend unit tests; backend untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Estimate tab to job details.
    • Estimate cost lines now support labour, material, and adjustment entries.
    • Added editing, autosave, deletion, and persistence for estimate cost lines.
  • Improvements

    • Draft changes save when leaving a row, reducing unintended requests.
    • Decimal values display in a cleaner format.
    • Labour selections preserve custom descriptions while updating generated ones appropriately.
    • Estimate time lines no longer require a Xero pay item.

corrinand others added 4 commits August 9, 2026 21:24
A thin kind=estimate CostLineGrid page (renders for every pricing
methodology — T&M jobs estimate too), lazy-wired. Numeric inputs display
wire decimals trimmed ('3.000' → '3'): typed values must round-trip as
typed for the estimate spec's string-equality assertions. The spec's
Tab chain (desc → quantity → unit cost → unit rev) is asserted to hold in
natural DOM order — no custom Tab handler to drift out of sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eager persist-on-completion swapped the draft row out from under the
focused unit-rev cell the moment a cost commit derived the revenue —
exactly the race v1's row-exit rule existed to prevent ('rapid edits to
Unit Revenue cannot be overwritten by an earlier POST response'). Field
commits now only update the draft; the tr's focusout posts when focus
leaves the whole row, and item picks still persist immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deviations from v1, recorded in the file: the shared job is created by the
first serial test through the standard authenticated fixture (no
hand-rolled beforeAll login); waitForAutosave replaces every sleep, armed
before the action that triggers the write; the row-exit gesture is a click
on the section heading (v1's custom Tab handler moved to the next row —
ours follows natural DOM order within the row); post-pick row finds retry
via toPass instead of v1's fixed sleeps, which papered over the
draft-to-server row swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep prose
The row-exit commit is deferred by a 0ms timer that any focus landing back
in the row's React tree cancels — the ItemSelect popover portals outside
the tr in the DOM, so the old relatedTarget containment check read opening
YOUR OWN picker as leaving the row, POSTing a complete draft as an
adjustment and silently discarding the pick. Draft deletion moves to
pointerdown (Safari does not focus buttons on click, so the preceding
null-relatedTarget blur would create the line being deleted).
parseDecimalInput canonicalises like the display so the send-dedupe
compares like with like (re-entering '25.00' over a shown '25' no longer
PATCHes and wipes an overridden revenue). Labour picks keep a user-typed
description (v1's rule) and the draft pick drops a stale stock binding.
trimDecimal guards to fixed-point forms. Ledgered: UI-created estimate
time lines store NULL xero_pay_item (load-bearing only for actual lines).
Findings: estimate-slice adversarial pair (1 blocker, 4 should-fix, 2 nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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:45 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: 3be3a4e9-1d6a-4c7a-b0fb-d766df081734

📥 Commits

Reviewing files that changed from the base of the PR and between e9ae359 and 0b7d308.

📒 Files selected for processing (2)
  • docs/rewrite-status.md
  • frontend/src/features/job/costing/CostLineGrid.tsx
📝 Walkthrough

Walkthrough

The PR adds an estimate tab, estimate cost-line interactions, deferred row-exit persistence, labour-rate description handling, decimal normalization, integration tests, E2E coverage, and rewrite-status documentation.

Changes

Estimate costing workflow

Layer / File(s)Summary
Costing rules and selection contracts
frontend/src/features/job/costing/calc.ts, frontend/src/features/job/costing/ItemSelect.tsx, frontend/src/features/job/costing/calc.test.ts
Decimal inputs are canonicalized. Labour selection receives all rates and preserves user-authored descriptions.
Deferred draft row persistence
frontend/src/features/job/costing/CostLineGrid.tsx, frontend/src/features/job/costing/CostLineGrid.test.tsx
Draft rows persist on row exit. Focus re-entry cancels pending commits. Draft deletion avoids unintended creation.
Estimate tab integration
frontend/src/features/job/JobDetailPage.tsx, frontend/src/features/job/costing/JobEstimateTab.tsx, frontend/src/features/job/costing/JobEstimateTab.test.tsx
Job details lazy-loads JobEstimateTab, which loads job defaults and renders CostLineGrid for estimates.
Estimate workflow validation and status
frontend/tests/e2e/job/create-estimate-entry.spec.ts, docs/accepted-api-differences.yml, docs/rewrite-status.md
E2E tests cover estimate entry creation, editing, replacement, deletion, and persistence. Documentation records API differences and rewrite progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant JobDetailPage
participant JobEstimateTab
participant JobDataAPI
participant CostLineGrid
participant PlaywrightE2E
JobDetailPage->>JobEstimateTab: open estimate tab
JobEstimateTab->>JobDataAPI: load job data and defaults
JobDataAPI-->>JobEstimateTab: return estimate configuration
JobEstimateTab->>CostLineGrid: render estimate rows
PlaywrightE2E->>CostLineGrid: create or edit cost line
CostLineGrid->>JobDataAPI: persist on row exit
JobDataAPI-->>CostLineGrid: return saved row state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 56.25% 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 estimate-tab configuration and the associated E2E progress.
Description check✅ PassedThe description explains the changes, rationale, tests, review fixes, and recorded deviations, but omits the template headings and checklist.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch job/estimate-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: 2

🧹 Nitpick comments (4)
frontend/src/features/job/costing/JobEstimateTab.test.tsx (1)

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

Rename the test to match what it asserts.

Nothing in this test varies the pricing methodology, and JobEstimateTab does not read job.pricing_methodology. The "renders for every pricing methodology" rule lives in the tab bar and JobDetailPage, which decide whether the estimate tab is offered. As written, the name suggests coverage that this file does not provide.

♻️ Proposed rename
- it('renders the estimate grid for any pricing methodology', async () => {+ it('renders the estimate cost-line grid from company defaults', async () => {
🤖 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/job/costing/JobEstimateTab.test.tsx` at line 36, Rename
the test case around JobEstimateTab to describe only the estimate grid rendering
behavior it actually asserts, removing the claim that it covers every pricing
methodology. Leave the test implementation unchanged.
frontend/src/features/job/costing/CostLineGrid.test.tsx (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ?? document.body fallback weakens this test.

fireEvent.pointerDown removes the draft synchronously, so React has already unmounted the unit-cost input by the time this line runs. The querySelector returns null and the fallback blurs document.body. The row tr no longer exists, so its onBlur never runs and the row-exit commit path is never exercised. The test then proves only that a removed row does not POST, which is weaker than the stated intent.

Assert the unmount explicitly instead of hiding it behind a fallback, so the test states what it verifies.

💚 Proposed change
 // pointerdown removes the draft before any blur-driven commit runs.
fireEvent.pointerDown(
document.querySelector<HTMLElement>('[data-automation-id="SmartCostLinesTable-delete-0"]')!,
)
- fireEvent.blur(- document.querySelector<HTMLInputElement>(- '[data-automation-id="SmartCostLinesTable-unit-cost-0"]',- ) ?? document.body,- )+ // The row is already gone, so the blur that Safari fires next lands on+ // nothing: there is no tr left to schedule a row-exit commit.+ expect(+ document.querySelector('[data-automation-id="SmartCostLinesTable-unit-cost-0"]'),+ ).toBeNull()+ fireEvent.blur(document.body)
🤖 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/job/costing/CostLineGrid.test.tsx` around lines 616 -
620, Update the test around the unit-cost input query to assert that the input
has been unmounted after fireEvent.pointerDown, rather than falling back to
document.body for fireEvent.blur. Keep the test focused on verifying the
intended post-removal behavior and ensure it does not imply that the row-exit
onBlur commit path still executes.
frontend/src/features/job/costing/calc.test.ts (1)

148-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the generic 'Labour' description.

labourPickDesc has three auto-fill branches: blank, the literal 'Labour', and another subtype's name. This test covers blank (via line 132) and the other-subtype case. The 'Labour' branch has no assertion, so a future reader can delete it without a failure.

💚 Proposed addition
 // Another subtype's auto-fill is replaced, not kept.
expect(
labourPickPatch(line({ desc: 'Office' }), {
rate: labourRate(),
wageRate: '38.00',
allRates: rates,
}).desc,
).toBe('Workshop')
+ // The generic v1 placeholder is auto-fill too, though no rate is named it.+ expect(+ labourPickPatch(line({ desc: 'Labour' }), {+ rate: labourRate(),+ wageRate: '38.00',+ allRates: rates,+ }).desc,+ ).toBe('Workshop')
})
🤖 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/job/costing/calc.test.ts` around lines 148 - 172, Add
an assertion in the “keeps a user-authored description (v1 rule)” test covering
a line whose description is exactly “Labour”, and verify labourPickPatch
replaces it with the expected selected subtype description (“Workshop”),
preserving the existing blank and other-subtype cases.
frontend/src/features/job/costing/CostLineGrid.tsx (1)

497-510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse labourPickPatch in the draft branch.

This block re-implements labourPickPatch inline: same kind, same labour_subtype, same labourPickDesc call, same wage/charge-out assignment, same stock_id strip. The stock branch directly above already avoids the copy by calling stockPickPatch(draftAsLine, ...). Two copies of the labour rule will drift the next time the rule changes.

draftAsLine already carries desc and ext_refs, so the helper produces the same values.

♻️ Proposed refactor
 onPickLabour={(rate: JobLabourRateOut, allRates: readonly JobLabourRateOut[]) => {
- // Same rules as the server-row patch: keep a user-authored desc,- // and drop any stale stock binding from a failed material pick.- const { stock_id: _dropped, ...keptRefs } = gridRow.draft.ext_refs- context.updateDraft(gridRow.localId, {- kind: 'time',- labour_subtype: rate.labour_subtype,- desc: labourPickDesc(gridRow.draft.desc, rate, allRates),- unit_cost: context.wageRate,- unit_rev: rate.charge_out_rate,- ext_refs: keptRefs,- })+ // One rule for both row types: the helper keeps a user-authored+ // desc and drops a stale stock binding from a failed material pick.+ const patch = labourPickPatch(draftAsLine, {+ rate,+ wageRate: context.wageRate,+ allRates,+ })+ context.updateDraft(gridRow.localId, {+ kind: 'time',+ labour_subtype: rate.labour_subtype,+ desc: patch.desc ?? '',+ unit_cost: typeof patch.unit_cost === 'string' ? patch.unit_cost : null,+ unit_rev: typeof patch.unit_rev === 'string' ? patch.unit_rev : null,+ ext_refs: patch.ext_refs ?? {},+ })
context.commitDraftField(gridRow.localId)
}}

The labourPickDesc import then becomes unused in this file.

As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."

🤖 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/job/costing/CostLineGrid.tsx` around lines 497 - 510,
Replace the inline draft update in the onPickLabour handler with the existing
labourPickPatch helper, passing draftAsLine and the selected rate data so it
preserves the same description, labour subtype, costs, and stock_id removal.
Keep the subsequent context.updateDraft and commit flow intact, and remove the
now-unused labourPickDesc import.

Source: Coding guidelines

🤖 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 `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 566-576: Update the draft-delete onPointerDown handler for
gridRow.type === 'draft' to call context.removeDraft only when the pointer event
represents a primary-button press, preserving the existing Safari blur-race
behavior while preventing right/middle clicks and non-primary pointer
interactions from deleting the draft.
- Around line 113-135: Add an unmount cleanup effect near rowExitTimersRef,
using the existing timer map to clear every pending timeout and remove its
entries when CostLineGrid unmounts. Update the React import to include
useEffect, without changing the existing scheduling or cancellation behavior.
---
Nitpick comments:
In `@frontend/src/features/job/costing/calc.test.ts`:
- Around line 148-172: Add an assertion in the “keeps a user-authored
description (v1 rule)” test covering a line whose description is exactly
“Labour”, and verify labourPickPatch replaces it with the expected selected
subtype description (“Workshop”), preserving the existing blank and
other-subtype cases.
In `@frontend/src/features/job/costing/CostLineGrid.test.tsx`:
- Around line 616-620: Update the test around the unit-cost input query to
assert that the input has been unmounted after fireEvent.pointerDown, rather
than falling back to document.body for fireEvent.blur. Keep the test focused on
verifying the intended post-removal behavior and ensure it does not imply that
the row-exit onBlur commit path still executes.
In `@frontend/src/features/job/costing/CostLineGrid.tsx`:
- Around line 497-510: Replace the inline draft update in the onPickLabour
handler with the existing labourPickPatch helper, passing draftAsLine and the
selected rate data so it preserves the same description, labour subtype, costs,
and stock_id removal. Keep the subsequent context.updateDraft and commit flow
intact, and remove the now-unused labourPickDesc import.
In `@frontend/src/features/job/costing/JobEstimateTab.test.tsx`:
- Line 36: Rename the test case around JobEstimateTab to describe only the
estimate grid rendering behavior it actually asserts, removing the claim that it
covers every pricing methodology. Leave the test implementation unchanged.
🪄 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: fc6032f3-2181-4960-aa12-5e03f633f38f

📥 Commits

Reviewing files that changed from the base of the PR and between 06038e3 and e9ae359.

📒 Files selected for processing (11)
  • docs/accepted-api-differences.yml
  • docs/rewrite-status.md
  • frontend/src/features/job/JobDetailPage.tsx
  • frontend/src/features/job/costing/CostLineGrid.test.tsx
  • frontend/src/features/job/costing/CostLineGrid.tsx
  • frontend/src/features/job/costing/ItemSelect.tsx
  • frontend/src/features/job/costing/JobEstimateTab.test.tsx
  • frontend/src/features/job/costing/JobEstimateTab.tsx
  • frontend/src/features/job/costing/calc.test.ts
  • frontend/src/features/job/costing/calc.ts
  • frontend/tests/e2e/job/create-estimate-entry.spec.ts

Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
Comment threadfrontend/src/features/job/costing/CostLineGrid.tsx
…egen
Draft deletion only acts on a primary-button pointerdown; pending row-exit
timers clear on unmount so a mid-blur tab navigation cannot fire a POST
into a gone grid; the status table row catches up with the new ledger
entry (the regen was missed after appending it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@corrin
corrin merged commit 913fc42 into mainAug 9, 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