Skip to content

Expand planning surfaces and booking capabilities#50

Merged
Ross1116 merged 11 commits into
mainfrom
staging
Mar 27, 2026
Merged

Expand planning surfaces and booking capabilities#50
Ross1116 merged 11 commits into
mainfrom
staging

Conversation

@Ross1116

@Ross1116 Ross1116 commented Mar 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Activity drilldown & context dialogs, upload review dialog, snapshot history panel, Planning Readiness card, and activity-linked / bulk booking flows.
  • Improvements

    • Interactive heatmap cells, provenance/source details in booking views, richer asset & contractor metadata, planning-readiness badges/filters, improved pagination/search behavior, and many UI sizing/spacing/color refinements.
  • Tests

    • Added lookahead and API-normalization test suites.

@vercel

vercel Bot commented Mar 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sitespace-app Ready Ready Preview, Comment Mar 28, 2026 1:10am

@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Converts many Tailwind arbitrary/variable classes to tokenized utilities; adds an extensive Lookahead/programme planning surface (API, hooks, types, normalization, UI components); implements activity-linked multi-date booking flows; extends asset/subcontractor/booking models with inference/planning fields; and makes RootLayout async to fetch an initial server user.

Changes

Cohort / File(s) Summary
Root & Auth
src/app/layout.tsx, src/app/(auth)/forgot-password/page.tsx, src/app/(auth)/login/page.tsx, src/app/(auth)/register/page.tsx, src/app/(auth)/reset-password/page.tsx, src/app/(auth)/set-password/page.tsx, src/app/not-found.tsx
Made RootLayout async and fetch initialUser; many Tailwind arbitrary/var → tokenized utility substitutions (e.g., bg-[var(--navy)]bg-navy).
Dashboard / Pages / Layout
src/app/(dashboard)/home/page.tsx, src/app/(dashboard)/layout.tsx, src/app/(dashboard)/loading.tsx, src/app/(dashboard)/assets/page.tsx, src/app/(dashboard)/subcontractors/page.tsx
Styling/token migrations and sizing tweaks; assets/subcontractors mapping extended with canonical/type-inference/planning fields; added readiness filter, planning-ready counts, badges, and sidebar fields.
Lookahead — API, hooks, keys, types, normalization & tests
src/hooks/lookahead/api.ts, src/hooks/lookahead/keys.ts, src/hooks/lookahead/useLookaheadQueries.ts, src/hooks/lookahead/useLookaheadData.ts, src/hooks/lookahead/api.test.ts, src/lib/apiNormalization.ts, src/lib/apiNormalization.test.ts, src/types/index.ts
Added normalization helpers, many lookahead API functions, typed SWR hooks and key builders, expanded domain types, and tests validating normalization and API shapes.
Lookahead — Components & Utilities
src/components/lookahead/..., src/components/lookahead/activityBookingCoverage.ts
Added multiple client dialogs/components (ActivityContextDialog, ActivityDrilldownDialog, UploadReviewDialog, SnapshotHistoryPanel, PlanningReadinessCard), activity coverage utilities, and rewrote LookaheadDashboard with centralized refresh/polling and new UI state.
CreateBookingForm & bookings UI
src/components/forms/CreateBookingForm.tsx, src/components/bookings/..., src/hooks/bookings/api.ts
CreateBookingForm adds activityContext prop, supports suggested-date bulk/multi-date planning, 15‑minute time normalization, per-date validation, and extended booking payload (programme_activity_id, selected_week_start); booking UIs now surface provenance/metadata.
Multicalendar & calendar helpers
src/components/multicalendar/..., src/lib/multicalendarHelpers.ts
Added booking provenance enrichment (provenanceSummary) to calendar events and booking-detail UI; sizing/class utility updates across calendar components.
UI primitives & general styling migration
src/components/*, src/components/ui/*
Mass migration from Tailwind bracket/arbitrary utilities to tokenized classes and numeric utilities, many flex-shrink-0shrink-0 changes, standardized focus ring syntax, z-index and sizing token updates across many components.
Normalization, subcontractors, project selection & helpers
src/lib/apiNormalization.ts, src/lib/subcontractorNormalization.ts, src/lib/bookingHelpers.ts, src/hooks/useProjectAssets.ts, src/hooks/useResolvedProjectSelection.ts
Added asOptionalString/asBoolean/asNumber helpers; extended normalization for assets/subcontractors/bookings; switched asset SWR fetcher to normalized payloads; improved project-selection hydration gating; added booking provenance/formatting helpers.
New tests
src/hooks/lookahead/api.test.ts, src/lib/apiNormalization.test.ts
New Vitest suites covering lookahead API normalization and apiNormalization behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DemandHeatmap
    participant ActivityDrilldownDialog
    participant ActivityContextDialog
    participant CreateBookingForm
    participant BookingAPI

    User->>DemandHeatmap: click heatmap cell
    DemandHeatmap->>ActivityDrilldownDialog: open(selectedCell)
    ActivityDrilldownDialog->>ActivityDrilldownDialog: fetch activities
    ActivityDrilldownDialog-->>User: show activities

    User->>ActivityDrilldownDialog: view context
    ActivityDrilldownDialog->>ActivityContextDialog: open(activity)
    ActivityContextDialog->>ActivityContextDialog: fetch booking context (suggested dates, candidates)
    ActivityContextDialog-->>User: show coverage & candidates

    User->>ActivityContextDialog: Book remaining
    ActivityContextDialog->>CreateBookingForm: open with activityContext
    CreateBookingForm->>CreateBookingForm: build multi-date plan, validate
    CreateBookingForm->>BookingAPI: submit payload (programme_activity_id, selected_week_start, ...)
    BookingAPI-->>User: confirm bookings
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I nibbled through types and Tailwind threads,

stitched lookahead dialogs and planned multi-date beds,
assets learned new names, readiness in view,
server fetched my user — the flow feels new,
a carrot‑cheer for bookings blooming true.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging

@coderabbitai coderabbitai Bot changed the title @coderabbitai Expand planning surfaces and booking capabilities Mar 27, 2026

@coderabbitai coderabbitai Bot 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: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/components/landing/TopBar.tsx (1)

106-135: ⚠️ Potential issue | 🔴 Critical

Fix Tailwind v4 arbitrary value syntax—missing bracket notation.

The classes use invalid Tailwind v4 syntax. Arbitrary spacing values and z-index utilities require bracket notation: gap-[1.25], w-[6.25], z-[100], z-[98], z-[99], max-w-[100], px-[7.5], pb-[7.5].

Without brackets, these classes won't apply any styles. Additionally, the z-index values (98, 99, 100) fall outside Tailwind v4's default scale and must use bracket syntax.

Invalid classes requiring fixes
  • gap-1.25gap-[1.25]
  • w-6.25w-[6.25] or w-[25px]
  • z-100, z-98, z-99z-[100], z-[98], z-[99]
  • max-w-100max-w-[100] or max-w-[25rem]
  • px-7.5px-[7.5] or px-[1.875rem]
  • pb-7.5pb-[7.5] or pb-[1.875rem]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/landing/TopBar.tsx` around lines 106 - 135, The Tailwind
classes in TopBar's JSX use invalid v4 arbitrary-value syntax; update the class
strings where the hamburger button, overlay button, and mobile menu div are
defined (refer to the JSX using isMenuOpen, toggleMenu, closeMenu) to use
bracket notation for non-standard values — e.g., change gap-1.25 → gap-[1.25],
w-6.25 → w-[6.25], z-100/z-99/z-98 → z-[100]/z-[99]/z-[98], max-w-100 →
max-w-[100] (or a valid rem/px value), and px-7.5/pb-7.5 → px-[7.5]/pb-[7.5];
ensure all arbitrary spacing/z-index classes in the button elements and the div
with transition-[right] use the bracketed form so Tailwind v4 will apply them.
src/app/(dashboard)/assets/page.tsx (2)

284-291: ⚠️ Potential issue | 🟠 Major

Search and type sorting still ignore the canonical label shown in the table.

Rows now render canonicalType || assetType, but the filter and the "assetType" sort path still read only asset.assetType. Assets whose inferred canonical type differs from the raw backend type won't be searchable or sortable by the value users actually see.

Also applies to: 303-337, 597-599

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 284 - 291, The filter and
sorting read only asset.assetType but the UI displays canonicalType ||
assetType; update all places that reference asset.assetType for searching and
sorting (e.g., the searchTerm filtering block using result.filter and any sort
comparator or sort key functions) to use the displayed value like
(asset.canonicalType || asset.assetType) so both search and sort operate on the
canonicalType when present; apply the same change where sorting logic reads
asset.assetType so the comparator uses the canonical fallback consistently.

273-275: ⚠️ Potential issue | 🟡 Minor

Reset pagination when the readiness filter changes.

Only searchTerm resets currentPage today. If someone is on a later page and switches to "Planning ready" or "Needs review", the table can render empty even though matching rows still exist.

Suggested fix
   useEffect(() => {
     setCurrentPage(1);
-  }, [searchTerm]);
+  }, [searchTerm, readinessFilter]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 273 - 275, The pagination
reset effect only watches searchTerm; update the useEffect that calls
setCurrentPage(1) so it also depends on the readiness filter (the state variable
controlling "Planning ready"/"Needs review")—either add that readiness state
(e.g., readinessFilter or readiness) to the dependency array of the existing
useEffect or create a separate useEffect that calls setCurrentPage(1) when
readiness changes; ensure you reference the same setCurrentPage and searchTerm
identifiers so the currentPage is reset whenever searchTerm or the readiness
filter changes.
src/components/forms/CreateBookingForm.tsx (1)

793-823: ⚠️ Potential issue | 🟠 Major

Don't let useProjectAssets errors blank out activity candidate assets.

In activity mode the form intentionally prefers candidateAssets, but this effect returns as soon as assetsSwrError is set. A transient project-assets failure will therefore hide the backend-provided candidate list and block activity-linked booking even though the dialog already has usable assets.

Suggested fix
   useEffect(() => {
-    if (assetsSwrError) {
+    const usingCandidateAssets = candidateAssets.length > 0;
+
+    if (assetsSwrError && !usingCandidateAssets) {
       dispatchAsset({ type: "SET_ASSET_ERROR", error: true });
       return;
     }
     const sourceAssets =
-      candidateAssets.length > 0
+      usingCandidateAssets
         ? candidateAssets.map((asset) => ({
             ...asset,
             asset_code: asset.asset_code || asset.id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/forms/CreateBookingForm.tsx` around lines 793 - 823, The
effect currently bails out whenever assetsSwrError is truthy, which wipes out
backend-provided candidateAssets; change the early-return to only set the error
and return when there are no candidateAssets (e.g. if (assetsSwrError &&
candidateAssets.length === 0) { dispatchAsset({ type: "SET_ASSET_ERROR", error:
true }); return; }), otherwise allow the effect to continue and use
candidateAssets as the sourceAssets; keep the rest of the normalization logic
(normalizedAssets, dispatchAsset({ type: "SET_ASSETS", ... })) and optionally
log the assetsSwrError without blocking candidateAssets when present.
🟡 Minor comments (12)
src/components/ui/checkbox.tsx-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor

Fix rounded-1 which is not a valid Tailwind v4 utility.

The class ring-3 is valid in Tailwind v4 (generates a 12px ring width), but rounded-1 does not exist in the standard v4 utilities. The default rounded scale includes rounded-xs (2px), rounded-sm (4px), rounded-md (6px), and others, but not rounded-1. Use rounded-sm instead, which provides the intended 4px border radius.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/checkbox.tsx` at line 17, In src/components/ui/checkbox.tsx
update the Tailwind class list used for the checkbox (the string in the checkbox
component) to replace the invalid utility "rounded-1" with the valid Tailwind v4
utility "rounded-sm"; locate the class string definition in the Checkbox
component and change only that token so the classes remain "peer ... rounded-sm
border ..." to restore a 4px border radius.
src/components/lookahead/WindowSelector.tsx-42-42 (1)

42-42: ⚠️ Potential issue | 🟡 Minor

Avoid shadowing the global window object.

Using window as the loop variable shadows the global window object, which could cause confusion or subtle bugs if window is referenced inside the callback.

🔧 Suggested fix
-          {(["2W", "4W", "6W"] as LookaheadWindowSize[]).map((window) => (
+          {(["2W", "4W", "6W"] as LookaheadWindowSize[]).map((size) => (
             <button
-              key={window}
+              key={size}
               type="button"
-              aria-pressed={windowSize === window}
-              onClick={() => onSetWindowSize(window)}
+              aria-pressed={windowSize === size}
+              onClick={() => onSetWindowSize(size)}
               className={`flex-1 rounded-lg px-3 py-2 text-sm font-semibold transition-all ${
-                windowSize === window
+                windowSize === size
                   ? "bg-navy text-white shadow-md shadow-slate-900/10"
                   : "text-slate-500 hover:text-slate-900"
               }`}
             >
-              {window === "2W"
+              {size === "2W"
                 ? "2 Weeks"
-                : window === "4W"
+                : size === "4W"
                   ? "4 Weeks"
                   : "6 Weeks"}
             </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/WindowSelector.tsx` at line 42, In WindowSelector's
JSX map (the (["2W","4W","6W"] as LookaheadWindowSize[]).map callback) the loop
parameter is named window which shadows the global window object; rename that
parameter to a non-conflicting identifier (e.g., win, size, or windowSize) and
update any references inside the map callback and its returned JSX (props, keys,
labels) accordingly to avoid shadowing and potential bugs.
src/components/multicalendar/MulticalendarPage.tsx-67-67 (1)

67-67: ⚠️ Potential issue | 🟡 Minor

Don’t overload description with provenance metadata.

CalendarEvent.description is consumed downstream as notes text, so after this change strings like Source: … and Modified from programme default will render under a “Notes” label. Keep provenance on dedicated event fields and compose it only in the views that intentionally display provenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/multicalendar/MulticalendarPage.tsx` at line 67, The
description field for CalendarEvent in MulticalendarPage.tsx is being overloaded
with provenance metadata; change the assignment so description is only the user
notes (e.g., description: b.notes || "") and stop appending provenanceSummary
there, leaving provenanceSummary on a dedicated event property (e.g., provenance
or provenanceSummary) so views that need provenance can compose it explicitly.
src/components/ui/full-calendar/calendar-views.tsx-423-428 (1)

423-428: ⚠️ Potential issue | 🟡 Minor

Offset the day-view “now” line by minutes, not just the hour.

Line 426 snaps the marker to the top of the current hour, so at 6:59 it still renders at 6:00. TimeTable already includes getMinutes(), which means the two live indicators drift apart for most of the hour.

Suggested fix
-  const currentHour = new Date().getHours();
-  const isCurrentDay = isSameDay(date, new Date());
+  const now = new Date();
+  const currentHour = now.getHours();
+  const currentMinutes = now.getMinutes();
+  const isCurrentDay = isSameDay(date, now);
...
-                  style={{ top: `${(currentHour - 6) * 48}px` }}
+                  style={{ top: `${((currentHour - 6) + currentMinutes / 60) * 48}px` }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/full-calendar/calendar-views.tsx` around lines 423 - 428,
The live "now" marker is anchored only to currentHour, causing it to snap to the
top of the hour; update the marker placement in the component (the conditional
render that uses isCurrentDay and currentHour for the div with className
"absolute w-full border-t-2 border-navy") to include minutes as well (use
TimeTable.getMinutes() or new Date().getMinutes()) so the style.top calculation
adds the fractional hour offset from minutes (minutes/60 * hourHeight) to the
existing (currentHour - 6) * hourHeight computation; adjust the top formula
accordingly so the marker aligns with the TimeTable minute-based indicator.
src/components/multicalendar/BookingDetailDialog.tsx-575-623 (1)

575-623: ⚠️ Potential issue | 🟡 Minor

Avoid the extra divider when provenance is absent.

Because there is already a separator just above this block, Line 622 leaves two dividers back-to-back when none of the provenance fields are present. Make the trailing <Separator /> part of the same condition, or collapse this into a single divider around the optional section.

Suggested structure
-                {(data.source ||
-                  data.programme_activity_name ||
-                  data.expected_asset_type ||
-                  data.booking_group_id ||
-                  data.is_modified) && (
-                  <div>
-                    ...
-                  </div>
-                )}
-
-                <Separator className="bg-slate-100" />
+                {(data.source ||
+                  data.programme_activity_name ||
+                  data.expected_asset_type ||
+                  data.booking_group_id ||
+                  data.is_modified) ? (
+                  <>
+                    <div>
+                      ...
+                    </div>
+                    <Separator className="bg-slate-100" />
+                  </>
+                ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/multicalendar/BookingDetailDialog.tsx` around lines 575 - 623,
The trailing Separator should only render when the Provenance block is present;
update the conditional that checks data.source, data.programme_activity_name,
data.expected_asset_type, data.booking_group_id, or data.is_modified to also
include the <Separator /> so it is rendered inside the same block (or wrap both
the provenance <div> and the Separator with the same condition) in the
BookingDetailDialog component, ensuring no back-to-back dividers appear when all
provenance fields are absent.
src/components/bookings/BookingCardDesktop.tsx-76-94 (1)

76-94: ⚠️ Potential issue | 🟡 Minor

Format bookingSource before showing it to users.

The normalized source value now includes enums like programme_activity; rendering that string directly exposes the internal value instead of a readable label. A tiny formatter here would also keep the mobile card and booking details dialog consistent.

Suggested tweak
+const formatSourceLabel = (source: string) =>
+  source
+    .replace(/[_-]+/g, " ")
+    .replace(/\b\w/g, (char) => char.toUpperCase());
...
-                <span className="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600 capitalize">
-                  {booking.bookingSource}
+                <span className="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600">
+                  {formatSourceLabel(booking.bookingSource)}
                 </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingCardDesktop.tsx` around lines 76 - 94,
BookingCardDesktop is rendering the raw enum-like booking.bookingSource (and
thus exposing values like "programme_activity") — convert the source to a
human-friendly label before rendering: create or use a formatter function (e.g.,
formatBookingSource) that maps/normalizes values to readable labels and call it
where booking.bookingSource is displayed in BookingCardDesktop (replace the
inline {booking.bookingSource} usage); also apply the same formatter in the
mobile card and BookingDetailsDialog to keep labels consistent across views.
src/components/lookahead/SnapshotHistoryPanel.tsx-33-50 (1)

33-50: ⚠️ Potential issue | 🟡 Minor

Expose the expand/collapse state on the toggle button.

This is acting as a disclosure, but the button on Lines 33-47 never reports whether the panel is open. Add aria-expanded and connect it to the content with aria-controls/id so screen readers announce the current state.

Suggested tweak
       <button
         type="button"
         onClick={() => setIsOpen(!isOpen)}
+        aria-expanded={isOpen}
+        aria-controls="lookahead-snapshot-history"
         className="flex w-full items-center justify-between px-5 py-3.5 text-sm font-bold text-slate-600 hover:text-slate-900"
       >
...
       {isOpen && (
-        <div className="space-y-2 border-t border-slate-100 p-3">
+        <div
+          id="lookahead-snapshot-history"
+          className="space-y-2 border-t border-slate-100 p-3"
+        >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/SnapshotHistoryPanel.tsx` around lines 33 - 50, The
toggle button for the Lookahead Snapshot History disclosure doesn't expose state
to assistive tech; update the button element (the one using isOpen and
setIsOpen) to include aria-expanded={isOpen} and aria-controls referencing the
content container, and give the content div a matching id (e.g.,
snapshot-history-panel or similar) so screen readers can announce open/closed
state and associate the button with the panel.
src/components/lookahead/PlanningReadinessCard.tsx-19-20 (1)

19-20: ⚠️ Potential issue | 🟡 Minor

Format the status label before rendering it.

The normalized payload already uses snake_case values such as at_risk, so this card will show raw enum text to users. Convert it to a display label here.

Suggested fix
   const { counts, actionable_tasks: tasks } = planningCompleteness;
   const score = Math.round(planningCompleteness.score ?? 0);
+  const statusLabel = (planningCompleteness.status ?? "").replace(/_/g, " ");
 
   return (
     <section className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
@@
         <div className="rounded-2xl bg-slate-50 px-4 py-3 text-center">
           <div className="text-3xl font-extrabold text-slate-900">{score}</div>
           <div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
-            {planningCompleteness.status}
+            {statusLabel}
           </div>
         </div>
       </div>

Also applies to: 37-40

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/PlanningReadinessCard.tsx` around lines 19 - 20, The
component PlanningReadinessCard is rendering raw enum text from
planningCompleteness (e.g., at_risk); compute a display label before rendering
by reading planningCompleteness.status (or similar status field), converting
snake_case to a user-friendly form (replace underscores with spaces and
title-case / capitalize words or map known enum values to explicit labels),
store it in a new variable like displayStatus and use that variable wherever the
status is rendered (also update the logic around counts/actionable_tasks/score
if they reference the raw status). Ensure you reference the existing symbols
planningCompleteness, counts, actionable_tasks (tasks), and score when locating
where to add the transformation.
src/components/lookahead/DemandHeatmap.tsx-116-124 (1)

116-124: ⚠️ Potential issue | 🟡 Minor

Hide the click hint when drilldown is disabled.

onCellSelect is optional, but this banner is unconditional. In non-interactive usages it tells users to click cells that are rendered as plain containers.

Suggested fix
-          <span className="inline-flex items-center gap-1 rounded-full bg-teal-gradient px-3 py-1 text-xs font-semibold text-navy">
-            <Sparkles className="h-3.5 w-3.5" />
-            Click any demand cell to drill into activity
-          </span>
+          {onCellSelect && (
+            <span className="inline-flex items-center gap-1 rounded-full bg-teal-gradient px-3 py-1 text-xs font-semibold text-navy">
+              <Sparkles className="h-3.5 w-3.5" />
+              Click any demand cell to drill into activity
+            </span>
+          )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/DemandHeatmap.tsx` around lines 116 - 124, The click
hint is always shown even when drilldown is disabled; update the DemandHeatmap
rendering so the hint span (the element containing Sparkles and "Click any
demand cell to drill into activity") is only rendered when the optional
onCellSelect prop is provided (i.e., truthy). Locate the JSX around timezone and
the hint span in the DemandHeatmap component and wrap or conditionally render
that hint using a check against onCellSelect so non-interactive usages do not
display the misleading instruction.
src/app/(dashboard)/subcontractors/page.tsx-619-634 (1)

619-634: ⚠️ Potential issue | 🟡 Minor

Don't render unknown readiness as "No".

selectedContractor.planningReady is optional, but this branch treats undefined the same as false. That will label records with missing readiness data as not ready in the sidebar, even though the table above already handles the missing state separately.

Suggested fix
                     <div className="flex items-center justify-between">
                       <div className="flex items-center gap-3">
                         <Briefcase className="h-4 w-4 text-slate-400" />
                         <span className="text-sm font-medium text-slate-500">
                           Planning Ready
                         </span>
                       </div>
-                      <span
-                        className={`text-sm font-semibold ${
-                          selectedContractor.planningReady
-                            ? "text-emerald-700"
-                            : "text-amber-700"
-                        }`}
-                      >
-                        {selectedContractor.planningReady ? "Yes" : "No"}
-                      </span>
+                      {selectedContractor.planningReady == null ? (
+                        <span className="text-sm font-semibold text-slate-500">
+                          Unknown
+                        </span>
+                      ) : (
+                        <span
+                          className={`text-sm font-semibold ${
+                            selectedContractor.planningReady
+                              ? "text-emerald-700"
+                              : "text-amber-700"
+                          }`}
+                        >
+                          {selectedContractor.planningReady ? "Yes" : "No"}
+                        </span>
+                      )}
                     </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/subcontractors/page.tsx around lines 619 - 634, The
sidebar currently treats undefined planningReady the same as false; change the
rendering logic around selectedContractor.planningReady in the component so it
distinguishes three states: true => show "Yes" with the emerald class, false =>
show "No" with the amber class, and undefined/null => show "Unknown" (or a
neutral placeholder) with a neutral text class; update both the conditional used
for the text and the className mapping (refer to
selectedContractor.planningReady in the JSX) to use strict checks (e.g., ===
true / === false) so missing data is not displayed as "No".
src/components/bookings/BookingsPage.tsx-253-260 (1)

253-260: ⚠️ Potential issue | 🟡 Minor

Refresh the default slot when the dialog opens.

With useMemo([]), nextHour and endHour are fixed at first render. If this page stays open for a while, opening the booking form later can prefill a past time range.

One low-impact option
   const { nextHour, endHour } = useMemo(() => {
     const now = new Date();
     const nextHour = startOfHour(addHours(now, 1));
     return {
       nextHour,
       endHour: addHours(nextHour, 1),
     };
-  }, []);
+  }, [isBookingFormOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingsPage.tsx` around lines 253 - 260, The values
nextHour and endHour are computed once due to useMemo([]) so they become stale;
update the logic to recompute when the booking dialog opens instead of only on
first render — for example remove the empty dependency useMemo and compute
nextHour/endHour inside the dialog open handler or change useMemo to depend on
the dialog open state (the dialog open boolean used in the component) so
nextHour = startOfHour(addHours(new Date(), 1)) and endHour = addHours(nextHour,
1) are recalculated when opening; look for useMemo, nextHour, endHour and the
dialog open handler or state to implement this change, using startOfHour and
addHours to build the new values.
src/components/lookahead/UploadReviewDialog.tsx-72-75 (1)

72-75: ⚠️ Potential issue | 🟡 Minor

The summary count can disagree with the table.

reviewRows falls back to mappings, but the "Needs review" card always renders unclassifiedMappings.length. When there are zero unclassified rows but non-empty mappings, the header says 0 even though the table still has items.

Suggested fix
               <p className="mt-1 text-lg font-bold text-amber-700">
-                {unclassifiedMappings.length}
+                {reviewRows.length}
               </p>

Also applies to: 111-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/UploadReviewDialog.tsx` around lines 72 - 75, The
"Needs review" count and the table use different sources: reviewRows uses the
fallback (unclassifiedMappings.length > 0 ? unclassifiedMappings : mappings) but
the card always displays unclassifiedMappings.length; update the card (and the
other occurrence at the block around lines 111-115) to derive its count from the
same variable reviewRows (or apply the same fallback logic) so the header count
matches the rows shown; reference reviewRows, unclassifiedMappings, and mappings
when making the change.
🧹 Nitpick comments (12)
src/components/TopNav.tsx (1)

67-69: Link the toggle button to the controlled menu for better SR context.

Add aria-controls on the button and a matching id on the mobile menu container.

♿ Suggested patch
-          <Button
+          <Button
             variant="ghost"
             onClick={() => setOpen(!isOpen)}
             className="p-2 text-navy hover:bg-white/40"
             aria-expanded={isOpen}
+            aria-controls="mobile-nav-menu"
             aria-label="Toggle navigation menu"
           >
-          <div className="absolute left-0 top-full w-full border-t border-white/55 bg-[linear-gradient(to_bottom,rgba(255,255,255,0.9),rgba(246,251,255,0.88))] shadow-[inset_0_1px_0_rgba(255,255,255,0.85),0_14px_36px_rgba(15,42,74,0.16)] backdrop-blur-[26px] lg:hidden">
+          <div id="mobile-nav-menu" className="absolute left-0 top-full w-full border-t border-white/55 bg-[linear-gradient(to_bottom,rgba(255,255,255,0.9),rgba(246,251,255,0.88))] shadow-[inset_0_1px_0_rgba(255,255,255,0.85),0_14px_36px_rgba(15,42,74,0.16)] backdrop-blur-[26px] lg:hidden">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TopNav.tsx` around lines 67 - 69, Add an aria-controls
attribute to the toggle button (the element with aria-expanded={isOpen} /
aria-label="Toggle navigation menu") and add a matching id on the mobile menu
container element that is shown/hidden by TopNav (the element that renders the
mobile menu controlled by isOpen); pick a unique id like "topnav-mobile-menu"
and use that exact id value in the button's aria-controls so screen readers can
associate the button with the controlled region.
src/components/lookahead/WindowSelector.tsx (1)

54-58: Consider a lookup object for cleaner label mapping.

The nested ternary can be replaced with a simple object lookup for improved readability.

♻️ Suggested refactor
const WINDOW_LABELS: Record<LookaheadWindowSize, string> = {
  "2W": "2 Weeks",
  "4W": "4 Weeks", 
  "6W": "6 Weeks",
};

// Then in JSX:
{WINDOW_LABELS[size]}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/WindowSelector.tsx` around lines 54 - 58, Replace
the nested ternary in WindowSelector.tsx with a typed lookup: create a constant
like WINDOW_LABELS: Record<LookaheadWindowSize, string> = { "2W":"2 Weeks",
"4W":"4 Weeks", "6W":"6 Weeks" } and use WINDOW_LABELS[window] in the JSX (or
the prop name used instead of window if different) to map the
LookaheadWindowSize to its label; this simplifies the rendering logic and
improves readability.
src/app/layout.tsx (1)

51-51: Consider logging authentication errors for observability.

The .catch(() => null) silently swallows any errors from getServerUser(). While falling back to null is appropriate for optional authentication, logging the error would aid debugging production issues.

📋 Suggested improvement
-  const initialUser = await getServerUser().catch(() => null);
+  const initialUser = await getServerUser().catch((err) => {
+    console.error("[RootLayout] Failed to fetch server user:", err);
+    return null;
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/layout.tsx` at line 51, The current call that sets initialUser
swallows errors from getServerUser() via .catch(() => null); change this to
capture the error and log it before returning null (e.g., .catch(err => { /* log
err with your app logger or console.error */ return null; })), referencing
getServerUser and initialUser so the error is observable; prefer using your
existing application logger (processLogger/requestLogger) if available instead
of console.error.
src/hooks/useResolvedProjectSelection.ts (1)

155-161: Minor: Consider extracting projectBootstrapLoading condition for clarity.

This boolean expression has grown complex with multiple conditions. Consider extracting it into named intermediate variables for better readability.

♻️ Suggested refactor
+ const isWaitingForProjects =
+   projectId === null && !projectsError && (projectsLoading || projectsRaw === undefined);
+
  const projectBootstrapLoading =
-   Boolean(userKey) &&
-   enabled &&
-   (!isProjectSelectionReady ||
-     (projectId === null &&
-       !projectsError &&
-       (projectsLoading || projectsRaw === undefined)));
+   Boolean(userKey) && enabled && (!isProjectSelectionReady || isWaitingForProjects);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useResolvedProjectSelection.ts` around lines 155 - 161, The boolean
for projectBootstrapLoading is too complex—extract intermediate named booleans
(e.g., hasUserKey = Boolean(userKey), needsBootstrap = enabled &&
!isProjectSelectionReady, noProjectYet = projectId === null && !projectsError &&
(projectsLoading || projectsRaw === undefined)) and then compute
projectBootstrapLoading = hasUserKey && (needsBootstrap || noProjectYet); update
references to use these names and add brief comments explaining each variable to
improve readability and maintainability (references: projectBootstrapLoading,
userKey, enabled, isProjectSelectionReady, projectId, projectsError,
projectsLoading, projectsRaw).
src/components/lookahead/VersionHistory.tsx (1)

17-23: Default Set creates a new instance on every render.

The default value deletingIds = new Set<string>() creates a new Set instance on each render when no prop is passed. While this won't cause bugs here since the Set is only read (via .has()), it's a subtle anti-pattern that could cause issues if passed to memoized children or used in dependency arrays.

♻️ Consider a module-level constant
+const EMPTY_SET = new Set<string>();
+
 export function VersionHistory({
   title = "Programme History",
   versions,
-  deletingIds = new Set<string>(),
+  deletingIds = EMPTY_SET,
   onDelete,
   onReviewUpload,
 }: Props) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/VersionHistory.tsx` around lines 17 - 23, The
default parameter deletingIds = new Set<string>() creates a new Set on every
render; define a module-level constant (e.g., const EMPTY_DELETING_IDS = new
Set<string>()) and use that instead (either as the default parameter for
VersionHistory or as a fallback inside the function via deletingIds ??
EMPTY_DELETING_IDS) so the same empty Set instance is reused across renders;
update references to the deletingIds parameter accordingly.
src/lib/subcontractorNormalization.ts (1)

33-34: Consider accepting numeric strings in asNumber.

If trade_inference_confidence comes back as "0.87"—which is common for decimal/score fields—the new branch normalizes it to null and the UI loses the value. Parsing finite numeric strings here would make this helper as tolerant as the rest of the adapter.

Suggested tweak
 const asNumber = (value: unknown): number | undefined =>
-  typeof value === "number" && Number.isFinite(value) ? value : undefined;
+  typeof value === "number" && Number.isFinite(value)
+    ? value
+    : typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))
+      ? Number(value)
+      : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/subcontractorNormalization.ts` around lines 33 - 34, The asNumber
helper currently only returns numbers and drops numeric strings; update asNumber
to also accept string values that represent finite numbers by trimming the
string, attempting to parse it (e.g., via Number or parseFloat), checking
Number.isFinite on the parsed result, and returning that numeric value; keep
returning undefined for non-numeric strings, empty strings, NaN, or non-finite
results so callers like trade_inference_confidence preserve numeric-string
inputs while still rejecting invalid values.
src/lib/apiNormalization.ts (1)

187-190: Normalize project_id with asId for consistency.

This is the only identifier in the new asset mapper that goes through asOptionalString, so numeric IDs would be discarded even though sibling fields accept them.

Suggested fix
-    project_id: asOptionalString(record.project_id),
+    project_id: asId(record.project_id) || undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/apiNormalization.ts` around lines 187 - 190, The project_id mapping
currently uses asOptionalString(record.project_id) which will drop numeric IDs;
change it to use asId(record.project_id) so IDs (numeric or string) are
normalized like sibling identifier fields; update any imports to include asId if
missing and keep the surrounding fields (planning_ready, description, status)
unchanged.
src/hooks/lookahead/api.ts (3)

382-385: Minor: Redundant truthy check.

The condition isRecord(payload.counts) && payload.counts is redundant since isRecord already ensures payload.counts is a non-null object.

Suggested simplification
   const countsRecord =
-    isRecord(payload.counts) && payload.counts
+    isRecord(payload.counts)
       ? payload.counts
       : (payload as Record<string, unknown>);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/lookahead/api.ts` around lines 382 - 385, The conditional creating
countsRecord redundantly checks truthiness after isRecord; simplify the
expression by using isRecord(payload.counts) ? payload.counts : (payload as
Record<string, unknown>) so countsRecord is set when isRecord(payload.counts) is
true and falls back to payload cast otherwise—update the code around
countsRecord and the isRecord(payload.counts) check accordingly.

155-173: Potential ambiguity: remaining_hours used as fallback for both hours and gap_hours.

If the API response contains only remaining_hours, this value will be assigned to hours (line 161), and gap_hours will be null (since the first ?? chain already consumed remaining_hours). This is likely the intended behavior, but worth verifying that the API doesn't expect both fields to receive the same fallback value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/lookahead/api.ts` around lines 155 - 173, The mapping currently
calls asNumber(entry.remaining_hours) inline so the first null-coalescing chain
will consume remaining_hours for hours and leave gap_hours null; to fix, extract
remaining once (e.g., const remaining = asNumber(entry.remaining_hours)) before
the return and use that same remaining as a fallback in both the hours and
gap_hours chains so both fields can receive remaining_hours when other candidate
fields are absent; keep using asNumber for other fields and update the hours and
gap_hours assignments in the same function in src/hooks/lookahead/api.ts
(references: asNumber, entry, hours, gap_hours, demand_hours).

215-227: Type assertion on entry.rows assumes valid row structure.

Line 224 uses a type assertion after the Array.isArray check, but doesn't validate that each element matches LookaheadRow. If the API returns malformed rows, this could cause runtime issues downstream. Consider adding row normalization if this API is prone to schema drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/lookahead/api.ts` around lines 215 - 227, The fetchLookaheadHistory
function trusts entry.rows via a raw type assertion to
LookaheadSnapshotResponse["rows"]; instead, validate/normalize each row to guard
against schema drift by replacing the assertion with an explicit mapping that
checks required fields (e.g., using a new isLookaheadRow type guard or inline
checks) and coerces/defaults values (use asOptionalString for string fields,
parse/ensure numeric fields, default missing arrays/objects), filtering out or
normalizing malformed rows so the returned rows array always matches the
LookaheadRow shape; update fetchLookaheadHistory (and introduce a small helper
like isLookaheadRow/normalizeLookaheadRow if needed) to perform this validation
before returning.
src/types/index.ts (1)

442-452: Consider making PlanningCompletenessTask.id required.

Line 443 declares id as optional, but normalizePlanningTasks in api.ts always provides a fallback ID (task-${index}). Making id required would align the type with the actual runtime guarantee.

Make id required
 export interface PlanningCompletenessTask {
-  id?: string;
+  id: string;
   title: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/index.ts` around lines 442 - 452, The interface
PlanningCompletenessTask currently marks id as optional but
normalizePlanningTasks in api.ts guarantees an id (using `task-${index}`) for
every task; update the PlanningCompletenessTask interface to make id required
(remove the optional marker) so the type reflects the runtime guarantee and
prevent callers from treating id as possibly undefined.
src/hooks/lookahead/useLookaheadQueries.ts (1)

61-78: The mutate wrapper discards parameters.

Line 75 wraps mutate as () => mutate(), losing the ability to pass data or options arguments. This prevents optimistic updates and revalidation with specific data. If this is intentional for simplicity, consider documenting it; otherwise, expose the full signature.

Expose full mutate signature
 type SwrResult<T> = {
   data: T | undefined;
   isLoading: boolean;
   error: unknown;
-  mutate: () => Promise<T | undefined>;
+  mutate: (data?: T | Promise<T | undefined> | ((currentData?: T) => T | Promise<T | undefined>), opts?: { revalidate?: boolean }) => Promise<T | undefined>;
   key: string | null;
 };

 function useTypedSWR<T>(
   key: string | null,
   fetcher: (() => Promise<T>) | null,
 ): SwrResult<T> {
   const { data, isLoading, error, mutate } = useSWR<T>(
     key,
     fetcher,
     SWR_CONFIG,
   );

   return {
     data,
     isLoading,
     error,
-    mutate: () => mutate(),
+    mutate,
     key,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/lookahead/useLookaheadQueries.ts` around lines 61 - 78, The wrapper
in useTypedSWR currently replaces SWR's mutate with a parameterless () =>
mutate(), which discards the original mutate signature and prevents passing
data/options for optimistic updates or revalidation; update useTypedSWR to
forward the full mutate signature (accepting data?: T | Promise<T> |
MutatorCallback<T>, shouldRevalidate?: boolean | undefined or the equivalent
used by your SWR types) and call the underlying mutate with those arguments, and
adjust the returned SwrResult<T> type if necessary so callers can pass through
data and options to mutate from useTypedSWR.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23fd0592-ade0-4b6a-a99d-0410d1fb5061

📥 Commits

Reviewing files that changed from the base of the PR and between 515eb38 and 2b32fd6.

📒 Files selected for processing (88)
  • src/app/(auth)/forgot-password/page.tsx
  • src/app/(auth)/login/page.tsx
  • src/app/(auth)/register/page.tsx
  • src/app/(auth)/reset-password/page.tsx
  • src/app/(auth)/set-password/page.tsx
  • src/app/(dashboard)/assets/page.tsx
  • src/app/(dashboard)/home/page.tsx
  • src/app/(dashboard)/layout.tsx
  • src/app/(dashboard)/loading.tsx
  • src/app/(dashboard)/subcontractors/page.tsx
  • src/app/layout.tsx
  • src/app/not-found.tsx
  • src/components/SideNav.tsx
  • src/components/TopNav.tsx
  • src/components/auth/ForgotPasswordForm.tsx
  • src/components/auth/LoginForm.tsx
  • src/components/auth/RegisterForm.tsx
  • src/components/auth/ResetPasswordForm.tsx
  • src/components/auth/SetPasswordForm.tsx
  • src/components/bookings/BookingCardDesktop.tsx
  • src/components/bookings/BookingCardDropdown.tsx
  • src/components/bookings/BookingCardMobile.tsx
  • src/components/bookings/BookingHistorySidebar.tsx
  • src/components/bookings/BookingList.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/forms/CreateAssetForm.tsx
  • src/components/forms/CreateBookingForm.tsx
  • src/components/forms/RescheduleBookingForm.tsx
  • src/components/forms/UpdateAssetForm.tsx
  • src/components/home/RadioToggle.tsx
  • src/components/landing/ClientDynamics.tsx
  • src/components/landing/ContactModal.tsx
  • src/components/landing/HeroOrbs.tsx
  • src/components/landing/HeroParallax.tsx
  • src/components/landing/LandingPage.tsx
  • src/components/landing/LookaheadPreview.tsx
  • src/components/landing/ShowcaseSection.tsx
  • src/components/landing/TopBar.tsx
  • src/components/landing/WatchVideoButton.tsx
  • src/components/lookahead/ActivityContextDialog.tsx
  • src/components/lookahead/ActivityDrilldownDialog.tsx
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/PlanningAlerts.tsx
  • src/components/lookahead/PlanningReadinessCard.tsx
  • src/components/lookahead/SnapshotHistoryPanel.tsx
  • src/components/lookahead/StatCards.tsx
  • src/components/lookahead/UploadReviewDialog.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/WindowSelector.tsx
  • src/components/lookahead/activityBookingCoverage.ts
  • src/components/multicalendar/AssetFilter.tsx
  • src/components/multicalendar/BookingDetailDialog.tsx
  • src/components/multicalendar/CalendarHeader.tsx
  • src/components/multicalendar/DesktopView.tsx
  • src/components/multicalendar/MobileView.tsx
  • src/components/multicalendar/MulticalendarPage.tsx
  • src/components/site-plans/SitePlanUploadDialog.tsx
  • src/components/site-plans/SitePlanViewerDialog.tsx
  • src/components/site-plans/SitePlansSection.tsx
  • src/components/ui/GlobalNetworkLoadingBar.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/ui/checkbox.tsx
  • src/components/ui/full-calendar/calendar-context.tsx
  • src/components/ui/full-calendar/calendar-utils.tsx
  • src/components/ui/full-calendar/calendar-views.tsx
  • src/components/ui/hero/hero-illustration.tsx
  • src/components/ui/hero/logo-carousel.tsx
  • src/components/ui/input.tsx
  • src/components/ui/navigation-menu.tsx
  • src/components/ui/radio-group.tsx
  • src/components/ui/select.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/tooltip.tsx
  • src/hooks/bookings/api.ts
  • src/hooks/lookahead/api.test.ts
  • src/hooks/lookahead/api.ts
  • src/hooks/lookahead/keys.ts
  • src/hooks/lookahead/useLookaheadData.ts
  • src/hooks/lookahead/useLookaheadQueries.ts
  • src/hooks/useProjectAssets.ts
  • src/hooks/useResolvedProjectSelection.ts
  • src/lib/apiNormalization.test.ts
  • src/lib/apiNormalization.ts
  • src/lib/subcontractorNormalization.ts
  • src/types/index.ts

Comment thread src/app/(dashboard)/assets/page.tsx Outdated
Comment thread src/components/forms/CreateBookingForm.tsx
Comment thread src/components/landing/HeroParallax.tsx Outdated
Comment thread src/components/lookahead/activityBookingCoverage.ts
Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx Outdated
Comment thread src/components/lookahead/UploadReviewDialog.tsx
Comment thread src/components/ui/hero/logo-carousel.tsx
Comment thread src/hooks/useProjectAssets.ts Outdated

@coderabbitai coderabbitai Bot 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: 3

🧹 Nitpick comments (9)
src/components/multicalendar/MulticalendarPage.tsx (1)

46-54: Use formatBookingSource when building provenanceSummary.

This keeps source labels consistent with booking cards/dialogs and avoids raw snake_case values in calendar surfaces.

♻️ Proposed refactor
 import { isAssetRetiredOrOutOfService } from "@/lib/assetStatus";
+import { formatBookingSource } from "@/lib/bookingHelpers";
...
-  const provenanceSummary = [
+  const sourceLabel = formatBookingSource(b.source);
+  const provenanceSummary = [
     b.programme_activity_name ? `Activity: ${b.programme_activity_name}` : null,
     b.expected_asset_type ? `Expected: ${b.expected_asset_type}` : null,
-    b.source ? `Source: ${b.source}` : null,
+    sourceLabel ? `Source: ${sourceLabel}` : null,
     b.is_modified ? "Modified from programme default" : null,
   ]

Also applies to: 68-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/multicalendar/MulticalendarPage.tsx` around lines 46 - 54, The
provenanceSummary currently uses raw b.source values; replace that usage with
the shared formatter formatBookingSource so source labels match booking
cards/dialogs. In the code that builds provenanceSummary (the array using
b.programme_activity_name, b.expected_asset_type, b.source, b.is_modified), call
formatBookingSource(b.source) instead of using b.source directly and ensure you
handle falsy/undefined returns the same way as other entries (filter(Boolean)).
Also apply the same change to the other provenanceSummary instance referenced in
the diff (the second occurrence that builds a similar array).
src/app/(dashboard)/subcontractors/page.tsx (2)

626-648: Consider simplifying the IIFE to a ternary chain for readability.

The IIFE works correctly but adds indentation depth. A ternary chain would be more concise and match React patterns:

♻️ Suggested simplification
-                      {(() => {
-                        if (selectedContractor.planningReady === true) {
-                          return (
-                            <span className="text-sm font-semibold text-emerald-700">
-                              Yes
-                            </span>
-                          );
-                        }
-
-                        if (selectedContractor.planningReady === false) {
-                          return (
-                            <span className="text-sm font-semibold text-amber-700">
-                              No
-                            </span>
-                          );
-                        }
-
-                        return (
-                          <span className="text-sm font-semibold text-slate-500">
-                            Unknown
-                          </span>
-                        );
-                      })()}
+                      <span
+                        className={`text-sm font-semibold ${
+                          selectedContractor.planningReady === true
+                            ? "text-emerald-700"
+                            : selectedContractor.planningReady === false
+                              ? "text-amber-700"
+                              : "text-slate-500"
+                        }`}
+                      >
+                        {selectedContractor.planningReady === true
+                          ? "Yes"
+                          : selectedContractor.planningReady === false
+                            ? "No"
+                            : "Unknown"}
+                      </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/subcontractors/page.tsx around lines 626 - 648, The IIFE
used to render the planning-ready status is verbose; replace the self-invoking
function with a concise ternary chain that checks
selectedContractor.planningReady (e.g., selectedContractor.planningReady ===
true ? ... : selectedContractor.planningReady === false ? ... : ...) and return
the same three span variants (text-sm font-semibold with text-emerald-700 for
"Yes", text-amber-700 for "No", and text-slate-500 for "Unknown") inside the JSX
to reduce nesting and improve readability while keeping the exact classes and
labels.

31-45: Consider using TransformedContractor from src/types/index.ts to avoid type drift.

The local Contractor interface duplicates TransformedContractor defined in src/types/index.ts (lines 232-246). Both interfaces define the same fields (suggestedTradeSpecialty, tradeResolutionStatus, tradeInferenceSource, tradeInferenceConfidence, planningReady).

If you need to keep a local type, consider extending or re-exporting the central type:

♻️ Suggested refactor
+import { TransformedContractor } from "@/types";
+
-interface Contractor {
-  contractorKey: string;
-  contractorName: string;
-  contractorCompany: string;
-  contractorTrade: string;
-  contractorEmail: string;
-  contractorPhone: string;
-  isActive: boolean;
-  suggestedTradeSpecialty?: string | null;
-  tradeResolutionStatus?: string | null;
-  tradeInferenceSource?: string | null;
-  tradeInferenceConfidence?: number | null;
-  planningReady?: boolean;
-  _originalData?: NormalizedSubcontractor;
-}
+type Contractor = TransformedContractor;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/subcontractors/page.tsx around lines 31 - 45, The local
Contractor interface duplicates fields from TransformedContractor; replace the
local declaration by importing and using TransformedContractor from src/types
(or declare interface Contractor extends TransformedContractor { _originalData?:
NormalizedSubcontractor } if you need the extra _originalData field) so you
avoid type drift—update any references to Contractor in this file to use the
imported/extended type and remove the duplicated interface declaration.
src/app/(dashboard)/assets/page.tsx (2)

481-498: Add aria-pressed for accessibility on toggle buttons.

These filter buttons function as toggle controls but lack the aria-pressed attribute to communicate the selected state to assistive technologies.

♿ Proposed fix for accessibility
                   <button
                     key={value}
                     type="button"
                     onClick={() => setReadinessFilter(value as ReadinessFilter)}
+                    aria-pressed={readinessFilter === value}
                     className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
                       readinessFilter === value
                         ? "bg-navy text-white"
                         : "bg-slate-100 text-slate-600 hover:bg-slate-200"
                     }`}
                   >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 481 - 498, The filter
buttons rendered in the map are interactive toggles but miss aria-pressed;
update the button elements that call setReadinessFilter and compare
readinessFilter to value (the elements in the array mapped to labels) to include
an aria-pressed attribute whose value is a boolean expression (readinessFilter
=== value) so assistive tech can detect the selected state; ensure this change
is applied to the button within the map that uses setReadinessFilter and
ReadinessFilter.

297-303: "Needs review" filter conflates false and undefined states.

The planningReady !== true condition (Line 302) groups both explicitly-not-ready (false) and unknown (undefined) assets under "Needs review". While this may be intentionally conservative, users might want to distinguish between:

  • Assets explicitly marked as not planning-ready
  • Assets with unknown/uncomputed readiness status

If this is the intended behavior, consider adding a tooltip or count breakdown to help users understand the mix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 297 - 303, The "review"
filter currently uses planningReady !== true which mixes false and undefined;
update the logic in the block that checks readinessFilter (symbol:
readinessFilter) so "review" only selects assets where asset.planningReady ===
false by changing result = result.filter((asset) => asset.planningReady !==
true) to a strict false check, and add a new branch (e.g., readinessFilter ===
"unknown") that filters for asset.planningReady === undefined if you want to
expose the unknown/uncomputed state separately (adjust UI labels/tooltips/counts
accordingly).
src/hooks/useProjectAssets.ts (1)

51-70: Pagination fallbacks silently mask malformed API responses.

The runtime type checks with fallback defaults (Lines 61-66) are defensive, but if the API consistently returns missing or malformed pagination fields, this will go unnoticed. The fallback has_more: false is particularly impactful as it prevents loading additional pages.

Consider adding debug/development logging when fallbacks are used, or ensure API contract tests validate the response shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProjectAssets.ts` around lines 51 - 70, The pagination fallback
logic in the useSWR fetcher (inside useProjectAssets.ts where swrKey,
getAssetEnvelope, normalizeAssetList and payload are used) silently masks
malformed/missing pagination fields (total, skip, limit, has_more); update the
fetcher to detect when you apply a fallback and emit a development-only warning
or debug log that includes the received payload and which fields were defaulted
so the issue is visible during dev/testing (use an existing logger or
console.debug/console.warn guarded by NODE_ENV !== 'production'), and ensure the
log references the swrKey and payload to aid troubleshooting.
src/components/lookahead/LookaheadDashboard.tsx (2)

1122-1136: Consider using the passed event data instead of a full workspace refresh.

The CreateBookingForm component passes the newly created events to the onSave callback (line 1131-1133), but the callback only calls refreshLookaheadWorkspace() and discards the event data. While the refresh will eventually fetch the new bookings from the server, using the passed events to update the local view would be more efficient and align with the component's intent to make bookings "appear immediately."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 1122 - 1136,
The onSave callback currently passed to CreateBookingForm discards the created
events and triggers a full refresh via refreshLookaheadWorkspace(); instead,
accept the events parameter from CreateBookingForm's onSave and merge them into
the local lookahead state so new bookings appear immediately (e.g., call the
existing state updater that holds bookings/events or a helper like
addEventsToWorkspace), and only call refreshLookaheadWorkspace() as a fallback
or background refresh if needed; update the handler passed to CreateBookingForm
(the onSave prop) to receive the created events and apply them to the same state
that the UI reads from, while keeping closeActivityFlow logic unchanged.

1042-1047: Use the isLoading flag from useLookaheadHistory instead of a heuristic.

The hook returns isLoading via its result spread (line 125), but the component only destructures history and mutate (line 191). Update the destructuring to include isLoading: historyLoading and use it directly instead of the history.length === 0 heuristic, which cannot distinguish between "still fetching" and "fetch completed with no results." This pattern is already used for other hooks in the same file (snapshotLoading, alertsLoading, versionsLoading).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 1042 - 1047,
The SnapshotHistoryPanel is using a heuristic for loading state; update the
useLookaheadHistory destructuring to include isLoading (rename to
historyLoading) from the hook (useLookaheadHistory) alongside history and
mutate, then replace the current isLoading prop expression on
SnapshotHistoryPanel with isSnapshotHistoryOpen && enabled && historyLoading so
it uses the hook-provided loading flag instead of history.length === 0; keep the
other props (history, onOpenChange, isOpen) unchanged.
src/components/ui/full-calendar/calendar-views.tsx (1)

1187-1188: Verify z-1000 is valid in Tailwind v4.In Tailwind v4, any z-index is allowed and generated despite not being in a predefined scale, so z-1000 is valid syntax. However, a z-index of 1000 for a tooltip is unusually high. Best practice is to layer tooltips at z-50 or similar to maintain a manageable stacking context.

Consider whether such a high value is necessary, or if z-50 would suffice. If z-1000 is intentional (e.g., to ensure tooltips appear above modals or other high-z elements), consider defining a named token like --z-index-tooltip: 1000 in your theme for consistency across the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/full-calendar/calendar-views.tsx` around lines 1187 - 1188,
The TooltipContent component is using a very large z-index class "z-1000";
change it to a more reasonable stacking value such as "z-50" (or another lower
value your design system uses) unless this high value is intentional; if 1000 is
required for cases where tooltips must overlay modals, replace the literal with
a named theme token (e.g., a CSS variable like --z-index-tooltip) and reference
that token via your Tailwind theme so TooltipContent's className stays
consistent across the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/bookings/BookingsPage.tsx`:
- Around line 164-169: Normalize the bookingSource before storing by trimming
whitespace and treating empty or whitespace-only strings as null: when setting
bookingSource (currently assigned from booking.source) call trim() on the value
and if the result is an empty string set bookingSource to null; keep other
fallback behavior (null) intact. Update the mapping that sets bookingSource so
downstream UI won't receive whitespace-only strings that render empty provenance
badges.

In `@src/components/multicalendar/BookingDetailDialog.tsx`:
- Around line 574-590: In BookingDetailDialog, the "Source:" row is rendered
when data.source is truthy even if formatBookingSource(data.source) returns an
empty string; change the guard so you compute the normalized label (call
formatBookingSource(data.source)) and only render the entire <p> block when that
normalizedLabel is non-empty (truthy); update the conditional around the Source
rendering to use the normalized label variable instead of raw data.source to
avoid blank rows.

In `@src/components/ui/full-calendar/calendar-views.tsx`:
- Line 876: The className on the element in calendar-views.tsx uses an invalid
Tailwind utility `rounded-0.75`; replace that token with a valid Tailwind
arbitrary value to preserve the 0.75rem (3px) radius — for example change
`rounded-0.75` to `rounded-[3px]` in the className string so the component's
styling (the className on the element in calendar-views.tsx) uses a supported
Tailwind v4 utility.

---

Nitpick comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 481-498: The filter buttons rendered in the map are interactive
toggles but miss aria-pressed; update the button elements that call
setReadinessFilter and compare readinessFilter to value (the elements in the
array mapped to labels) to include an aria-pressed attribute whose value is a
boolean expression (readinessFilter === value) so assistive tech can detect the
selected state; ensure this change is applied to the button within the map that
uses setReadinessFilter and ReadinessFilter.
- Around line 297-303: The "review" filter currently uses planningReady !== true
which mixes false and undefined; update the logic in the block that checks
readinessFilter (symbol: readinessFilter) so "review" only selects assets where
asset.planningReady === false by changing result = result.filter((asset) =>
asset.planningReady !== true) to a strict false check, and add a new branch
(e.g., readinessFilter === "unknown") that filters for asset.planningReady ===
undefined if you want to expose the unknown/uncomputed state separately (adjust
UI labels/tooltips/counts accordingly).

In `@src/app/`(dashboard)/subcontractors/page.tsx:
- Around line 626-648: The IIFE used to render the planning-ready status is
verbose; replace the self-invoking function with a concise ternary chain that
checks selectedContractor.planningReady (e.g., selectedContractor.planningReady
=== true ? ... : selectedContractor.planningReady === false ? ... : ...) and
return the same three span variants (text-sm font-semibold with text-emerald-700
for "Yes", text-amber-700 for "No", and text-slate-500 for "Unknown") inside the
JSX to reduce nesting and improve readability while keeping the exact classes
and labels.
- Around line 31-45: The local Contractor interface duplicates fields from
TransformedContractor; replace the local declaration by importing and using
TransformedContractor from src/types (or declare interface Contractor extends
TransformedContractor { _originalData?: NormalizedSubcontractor } if you need
the extra _originalData field) so you avoid type drift—update any references to
Contractor in this file to use the imported/extended type and remove the
duplicated interface declaration.

In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 1122-1136: The onSave callback currently passed to
CreateBookingForm discards the created events and triggers a full refresh via
refreshLookaheadWorkspace(); instead, accept the events parameter from
CreateBookingForm's onSave and merge them into the local lookahead state so new
bookings appear immediately (e.g., call the existing state updater that holds
bookings/events or a helper like addEventsToWorkspace), and only call
refreshLookaheadWorkspace() as a fallback or background refresh if needed;
update the handler passed to CreateBookingForm (the onSave prop) to receive the
created events and apply them to the same state that the UI reads from, while
keeping closeActivityFlow logic unchanged.
- Around line 1042-1047: The SnapshotHistoryPanel is using a heuristic for
loading state; update the useLookaheadHistory destructuring to include isLoading
(rename to historyLoading) from the hook (useLookaheadHistory) alongside history
and mutate, then replace the current isLoading prop expression on
SnapshotHistoryPanel with isSnapshotHistoryOpen && enabled && historyLoading so
it uses the hook-provided loading flag instead of history.length === 0; keep the
other props (history, onOpenChange, isOpen) unchanged.

In `@src/components/multicalendar/MulticalendarPage.tsx`:
- Around line 46-54: The provenanceSummary currently uses raw b.source values;
replace that usage with the shared formatter formatBookingSource so source
labels match booking cards/dialogs. In the code that builds provenanceSummary
(the array using b.programme_activity_name, b.expected_asset_type, b.source,
b.is_modified), call formatBookingSource(b.source) instead of using b.source
directly and ensure you handle falsy/undefined returns the same way as other
entries (filter(Boolean)). Also apply the same change to the other
provenanceSummary instance referenced in the diff (the second occurrence that
builds a similar array).

In `@src/components/ui/full-calendar/calendar-views.tsx`:
- Around line 1187-1188: The TooltipContent component is using a very large
z-index class "z-1000"; change it to a more reasonable stacking value such as
"z-50" (or another lower value your design system uses) unless this high value
is intentional; if 1000 is required for cases where tooltips must overlay
modals, replace the literal with a named theme token (e.g., a CSS variable like
--z-index-tooltip) and reference that token via your Tailwind theme so
TooltipContent's className stays consistent across the codebase.

In `@src/hooks/useProjectAssets.ts`:
- Around line 51-70: The pagination fallback logic in the useSWR fetcher (inside
useProjectAssets.ts where swrKey, getAssetEnvelope, normalizeAssetList and
payload are used) silently masks malformed/missing pagination fields (total,
skip, limit, has_more); update the fetcher to detect when you apply a fallback
and emit a development-only warning or debug log that includes the received
payload and which fields were defaulted so the issue is visible during
dev/testing (use an existing logger or console.debug/console.warn guarded by
NODE_ENV !== 'production'), and ensure the log references the swrKey and payload
to aid troubleshooting.
🪄 Autofix (Beta)

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

Run ID: c4b36e88-141d-436c-aa5f-47e38981f723

📥 Commits

Reviewing files that changed from the base of the PR and between 2b32fd6 and 28b6254.

📒 Files selected for processing (22)
  • src/app/(dashboard)/assets/page.tsx
  • src/app/(dashboard)/subcontractors/page.tsx
  • src/components/bookings/BookingCardDesktop.tsx
  • src/components/bookings/BookingCardMobile.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/forms/CreateBookingForm.tsx
  • src/components/landing/HeroParallax.tsx
  • src/components/landing/LandingPage.tsx
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/PlanningReadinessCard.tsx
  • src/components/lookahead/SnapshotHistoryPanel.tsx
  • src/components/lookahead/UploadReviewDialog.tsx
  • src/components/lookahead/WindowSelector.tsx
  • src/components/lookahead/activityBookingCoverage.ts
  • src/components/multicalendar/BookingDetailDialog.tsx
  • src/components/multicalendar/MulticalendarPage.tsx
  • src/components/ui/checkbox.tsx
  • src/components/ui/full-calendar/calendar-views.tsx
  • src/hooks/useProjectAssets.ts
  • src/lib/bookingHelpers.ts
  • src/lib/multicalendarHelpers.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/ui/checkbox.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/components/landing/HeroParallax.tsx
  • src/components/lookahead/WindowSelector.tsx
  • src/components/lookahead/PlanningReadinessCard.tsx
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/bookings/BookingCardDesktop.tsx
  • src/components/lookahead/activityBookingCoverage.ts
  • src/components/landing/LandingPage.tsx
  • src/components/forms/CreateBookingForm.tsx

Comment thread src/components/bookings/BookingsPage.tsx Outdated
Comment thread src/components/multicalendar/BookingDetailDialog.tsx Outdated
Comment thread src/components/ui/full-calendar/calendar-views.tsx Outdated

@coderabbitai coderabbitai Bot 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/bookings/BookingsPage.tsx (1)

223-234: ⚠️ Potential issue | 🟡 Minor

Potential indefinite loading state if bootstrap never resolves.

The redirect is now suppressed while projectBootstrapLoading is true. Per the hook implementation (context snippet 1), this flag can remain true indefinitely if:

  • The Zustand store's hasHydrated never becomes true, or
  • A fetch error occurs that doesn't set projectsError

This would leave users stuck on this page with an endless loading spinner (line 442) and no redirect to /home.

Consider whether a timeout fallback is warranted in useResolvedProjectSelection to prevent this edge case from degrading UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingsPage.tsx` around lines 223 - 234, The
redirect suppression using projectBootstrapLoading can hang indefinitely if the
bootstrap never resolves; update the useResolvedProjectSelection hook (or the
logic that sets projectBootstrapLoading) to include a timeout fallback that
forces bootstrap completion after a reasonable delay (e.g., 5–10s) or when
hydration fails, and expose a fallback flag (e.g., projectBootstrapTimedOut) so
the BookingsPage redirect logic can treat timeout as resolved; specifically
modify useResolvedProjectSelection (and any store setter that manages
projectBootstrapLoading/hasResolvedProjects) to clear projectBootstrapLoading
and set a timed-out state on timeout or on fetch error so window.location.href =
"/home" can run instead of leaving the spinner indefinitely.
src/app/(dashboard)/assets/page.tsx (1)

782-791: ⚠️ Potential issue | 🟡 Minor

Don’t present the raw type as canonical.

When canonicalType is missing, this still renders assetType under the “Canonical Type” label. That makes unresolved assets look normalized and conflicts with the adjacent resolution state.

Suggested fix
-                        {selectedAsset.canonicalType || selectedAsset.assetType}
+                        {selectedAsset.canonicalType || "Unspecified"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 782 - 791, The Canonical
Type field currently falls back to selectedAsset.assetType when
selectedAsset.canonicalType is falsy; change this so the UI does not present raw
assetType as canonical — update the JSX that renders the Canonical Type (the
span using selectedAsset.canonicalType || selectedAsset.assetType) to only
render selectedAsset.canonicalType and otherwise render a neutral indicator
(empty string, placeholder like "—" or "Unresolved") so unresolved assets aren’t
shown as normalized; ensure you update the span that references
selectedAsset.canonicalType and selectedAsset.assetType accordingly.
♻️ Duplicate comments (1)
src/hooks/useProjectAssets.ts (1)

24-40: ⚠️ Potential issue | 🟡 Minor

Search nested records envelopes too.

Line 27 already treats records as a valid list wrapper, but getAssetEnvelope() never descends into that key. For payloads like { records: { assets: [...], total: ... } }, the assets can still normalize while pagination falls back again.

Suggested fix
-  for (const key of ["data", "assets", "results", "items"]) {
+  for (const key of ["data", "assets", "results", "items", "records"]) {
     const nestedValue = payload[key];
     if (hasAssetList(nestedValue)) {
       return nestedValue;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProjectAssets.ts` around lines 24 - 40, getAssetEnvelope()
currently doesn't descend into a nested "records" envelope even though
hasAssetList() recognizes "records"; update the for-loop in getAssetEnvelope to
include "records" in the keys array (replace ["data","assets","results","items"]
with ["data","assets","results","items","records"]) so the function will detect
and return nested payload.records that contain an asset list; keep the existing
hasAssetList(payload) check and return behavior unchanged.
🧹 Nitpick comments (4)
src/components/bookings/BookingsPage.tsx (1)

253-260: Clarify the intent of the isBookingFormOpen dependency.

The memoization recalculates nextHour/endHour only when isBookingFormOpen changes. This means:

  • When the form opens (false → true), fresh times are computed ✓
  • While the form stays open, times remain frozen (acceptable for a booking form)

This behavior is reasonable, but the dependency choice is non-obvious. Consider adding a brief comment explaining the intent to prevent future maintainers from "fixing" it.

📝 Suggested clarifying comment
  const { nextHour, endHour } = useMemo(() => {
+   // Recompute default times only when the form opens (not on every render)
    const now = new Date();
    const nextHour = startOfHour(addHours(now, 1));
    return {
      nextHour,
      endHour: addHours(nextHour, 1),
    };
  }, [isBookingFormOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingsPage.tsx` around lines 253 - 260, Add a short
clarifying comment above the useMemo that computes nextHour and endHour in the
BookingsPage component explaining that the memo intentionally depends on
isBookingFormOpen so that times are recalculated only when the booking form is
opened/closed (to freeze times while the form is open), e.g., reference
nextHour, endHour, and isBookingFormOpen in the comment to prevent future
maintainers from removing or changing this dependency.
src/components/multicalendar/MulticalendarPage.tsx (1)

46-55: Potential duplication with BookingDetailDialog provenance rendering.

This provenanceSummary computation mirrors the provenance section in BookingDetailDialog.tsx (lines 575-620), which independently reads the same raw fields (programme_activity_name, expected_asset_type, source, is_modified) and formats them for display.

Having two parallel provenance representations risks drift if one is updated without the other. Consider extracting a shared utility (e.g., buildProvenanceSummary(booking)) to ensure consistency.

♻️ Example shared utility
// src/lib/bookingHelpers.ts
export const buildProvenanceSummary = (booking: {
  programme_activity_name?: string | null;
  expected_asset_type?: string | null;
  source?: string | null;
  is_modified?: boolean;
}): string => {
  const sourceLabel = formatBookingSource(booking.source);
  return [
    booking.programme_activity_name ? `Activity: ${booking.programme_activity_name}` : null,
    booking.expected_asset_type ? `Expected: ${booking.expected_asset_type}` : null,
    sourceLabel ? `Source: ${sourceLabel}` : null,
    booking.is_modified ? "Modified from programme default" : null,
  ]
    .filter(Boolean)
    .join(" · ");
};

Then use it in both MulticalendarPage.tsx and potentially in BookingDetailDialog.tsx for the summary tooltip/badge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/multicalendar/MulticalendarPage.tsx` around lines 46 - 55, The
provenance summary logic in MulticalendarPage (provenanceSummary using
formatBookingSource and fields programme_activity_name, expected_asset_type,
source, is_modified) duplicates the same rendering in BookingDetailDialog;
extract this into a shared helper (e.g., buildProvenanceSummary(booking)) that
accepts the booking object, calls formatBookingSource(booking.source) and
returns the joined string, then replace the inline provenanceSummary in
MulticalendarPage and the equivalent block in BookingDetailDialog to call
buildProvenanceSummary to ensure a single source of truth.
src/components/lookahead/LookaheadDashboard.tsx (1)

270-291: Consider guarding against null projectId in revalidation.

revalidateBookingsForProject(projectId) is called without checking if projectId is non-null. While Promise.allSettled handles failures gracefully and current call sites likely have valid projectId, adding a guard improves defensive coding.

🔧 Optional defensive improvement
 const refreshLookaheadWorkspace = useCallback(async () => {
-  await Promise.allSettled([
+  const mutations = [
     mutateSnapshot(),
     mutateAlerts(),
     mutateHistory(),
     mutateVersions(),
     mutateUploadStatus(),
     mutateActivities(),
     mutateBookingContext(),
-    revalidateBookingsForProject(projectId),
-  ]);
+  ];
+  if (projectId) {
+    mutations.push(revalidateBookingsForProject(projectId));
+  }
+  await Promise.allSettled(mutations);
 }, [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 270 - 291, In
refreshLookaheadWorkspace, guard against a null/undefined projectId before
calling revalidateBookingsForProject(projectId): check projectId and only
include the revalidation promise when it's truthy (e.g., conditionally push or
call revalidateBookingsForProject inside an if) so you don't pass null into
revalidateBookingsForProject; update the Promise.allSettled array construction
in refreshLookaheadWorkspace to conditionally include the
revalidateBookingsForProject call and keep the other mutate* calls unchanged.
src/app/(dashboard)/assets/page.tsx (1)

46-67: Reuse the shared ApiAsset type here.

useProjectAssets() already returns normalized ApiAsset[], so keeping a parallel AssetFromBackend contract in this file means these backend fields now have to stay in sync in two places.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 46 - 67, AssetFromBackend
duplicates the shared ApiAsset contract; replace the local AssetFromBackend type
with the exported ApiAsset type and update any references in this file (e.g.,
where useProjectAssets() consumers expect ApiAsset[]) so the file imports
ApiAsset instead of declaring AssetFromBackend; remove the AssetFromBackend
interface and ensure any nullable/optional fields align with ApiAsset to avoid
type mismatches when using useProjectAssets().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 297-303: The "Needs review" filter currently uses strict === false
and misses assets with planningReady === undefined; update the readinessFilter
handling so the "review" branch selects assets where planningReady is not true
(e.g., asset.planningReady === false || asset.planningReady === undefined or
equivalently asset.planningReady !== true) instead of only === false; modify the
block that checks readinessFilter === "review" (and the lambda asset => ...) to
include undefined values so unmarked assets appear in the Needs review bucket.
- Around line 635-654: The current outer guard hides the whole badge row when
asset.planningReady is undefined, which also suppresses
asset.typeResolutionStatus; change the outer condition to render the wrapper
when either asset.planningReady is defined or asset.typeResolutionStatus exists
(e.g. if (asset.planningReady !== undefined || asset.typeResolutionStatus)),
keep the planning-ready span rendered only when asset.planningReady !==
undefined and keep the type resolution span rendered when
asset.typeResolutionStatus is truthy so the status badge shows independently.

---

Outside diff comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 782-791: The Canonical Type field currently falls back to
selectedAsset.assetType when selectedAsset.canonicalType is falsy; change this
so the UI does not present raw assetType as canonical — update the JSX that
renders the Canonical Type (the span using selectedAsset.canonicalType ||
selectedAsset.assetType) to only render selectedAsset.canonicalType and
otherwise render a neutral indicator (empty string, placeholder like "—" or
"Unresolved") so unresolved assets aren’t shown as normalized; ensure you update
the span that references selectedAsset.canonicalType and selectedAsset.assetType
accordingly.

In `@src/components/bookings/BookingsPage.tsx`:
- Around line 223-234: The redirect suppression using projectBootstrapLoading
can hang indefinitely if the bootstrap never resolves; update the
useResolvedProjectSelection hook (or the logic that sets
projectBootstrapLoading) to include a timeout fallback that forces bootstrap
completion after a reasonable delay (e.g., 5–10s) or when hydration fails, and
expose a fallback flag (e.g., projectBootstrapTimedOut) so the BookingsPage
redirect logic can treat timeout as resolved; specifically modify
useResolvedProjectSelection (and any store setter that manages
projectBootstrapLoading/hasResolvedProjects) to clear projectBootstrapLoading
and set a timed-out state on timeout or on fetch error so window.location.href =
"/home" can run instead of leaving the spinner indefinitely.

---

Duplicate comments:
In `@src/hooks/useProjectAssets.ts`:
- Around line 24-40: getAssetEnvelope() currently doesn't descend into a nested
"records" envelope even though hasAssetList() recognizes "records"; update the
for-loop in getAssetEnvelope to include "records" in the keys array (replace
["data","assets","results","items"] with
["data","assets","results","items","records"]) so the function will detect and
return nested payload.records that contain an asset list; keep the existing
hasAssetList(payload) check and return behavior unchanged.

---

Nitpick comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 46-67: AssetFromBackend duplicates the shared ApiAsset contract;
replace the local AssetFromBackend type with the exported ApiAsset type and
update any references in this file (e.g., where useProjectAssets() consumers
expect ApiAsset[]) so the file imports ApiAsset instead of declaring
AssetFromBackend; remove the AssetFromBackend interface and ensure any
nullable/optional fields align with ApiAsset to avoid type mismatches when using
useProjectAssets().

In `@src/components/bookings/BookingsPage.tsx`:
- Around line 253-260: Add a short clarifying comment above the useMemo that
computes nextHour and endHour in the BookingsPage component explaining that the
memo intentionally depends on isBookingFormOpen so that times are recalculated
only when the booking form is opened/closed (to freeze times while the form is
open), e.g., reference nextHour, endHour, and isBookingFormOpen in the comment
to prevent future maintainers from removing or changing this dependency.

In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 270-291: In refreshLookaheadWorkspace, guard against a
null/undefined projectId before calling revalidateBookingsForProject(projectId):
check projectId and only include the revalidation promise when it's truthy
(e.g., conditionally push or call revalidateBookingsForProject inside an if) so
you don't pass null into revalidateBookingsForProject; update the
Promise.allSettled array construction in refreshLookaheadWorkspace to
conditionally include the revalidateBookingsForProject call and keep the other
mutate* calls unchanged.

In `@src/components/multicalendar/MulticalendarPage.tsx`:
- Around line 46-55: The provenance summary logic in MulticalendarPage
(provenanceSummary using formatBookingSource and fields programme_activity_name,
expected_asset_type, source, is_modified) duplicates the same rendering in
BookingDetailDialog; extract this into a shared helper (e.g.,
buildProvenanceSummary(booking)) that accepts the booking object, calls
formatBookingSource(booking.source) and returns the joined string, then replace
the inline provenanceSummary in MulticalendarPage and the equivalent block in
BookingDetailDialog to call buildProvenanceSummary to ensure a single source of
truth.
🪄 Autofix (Beta)

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

Run ID: 7d2481d8-e370-418a-b72c-a663b03df336

📥 Commits

Reviewing files that changed from the base of the PR and between 28b6254 and 9a1aacb.

📒 Files selected for processing (10)
  • src/app/(dashboard)/assets/page.tsx
  • src/app/(dashboard)/home/page.tsx
  • src/app/(dashboard)/subcontractors/page.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/multicalendar/BookingDetailDialog.tsx
  • src/components/multicalendar/MulticalendarPage.tsx
  • src/components/ui/full-calendar/calendar-views.tsx
  • src/components/ui/hero/logo-carousel.tsx
  • src/hooks/useProjectAssets.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/ui/hero/logo-carousel.tsx
  • src/app/(dashboard)/home/page.tsx
  • src/app/(dashboard)/subcontractors/page.tsx
  • src/components/multicalendar/BookingDetailDialog.tsx
  • src/components/ui/full-calendar/calendar-views.tsx

Comment thread src/app/(dashboard)/assets/page.tsx
Comment thread src/app/(dashboard)/assets/page.tsx Outdated

@coderabbitai coderabbitai Bot 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: 1

🧹 Nitpick comments (3)
src/components/lookahead/LookaheadDashboard.tsx (2)

1133-1147: Minor: Redundant isOpen prop.

The outer conditional already ensures selectedActivity is truthy, making isOpen={Boolean(selectedActivity)} always true. Consider simplifying to isOpen={true} or removing if the component supports it.

♻️ Simplify redundant prop
         <CreateBookingForm
-          isOpen={Boolean(selectedActivity)}
+          isOpen
           onClose={closeActivityFlow}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 1133 - 1147,
The CreateBookingForm is always rendered only when selectedActivity is truthy,
so the isOpen={Boolean(selectedActivity)} prop is redundant; update the JSX
around CreateBookingForm (the conditional that checks activityDialogMode,
selectedActivity, bookingContext, and bookingContextLoading) to either pass
isOpen={true} or remove the isOpen prop entirely if CreateBookingForm treats
absence as open—adjust callers/props accordingly (look at CreateBookingForm
usage and prop interface) and ensure closeActivityFlow and
refreshLookaheadWorkspace behavior remains unchanged.

85-90: Time normalization may not handle edge cases correctly.

The normalization assumes time is either HH:MM (5 chars) or HH:MM:SS (8 chars). If the API returns times with single-digit hours (e.g., 9:30) or other formats, this could produce invalid results.

♻️ Consider a more robust normalization
 function toDateTime(date?: string | null, time?: string | null): Date | null {
   if (!date || !time) return null;
-  const normalizedTime = time.length === 5 ? `${time}:00` : time;
+  const normalizedTime = time.includes(":") && !time.match(/:\d{2}$/)
+    ? `${time}:00`
+    : time;
   const parsed = new Date(`${date}T${normalizedTime}`);
   return Number.isNaN(parsed.getTime()) ? null : parsed;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 85 - 90, The
toDateTime function's time normalization assumes fixed lengths and fails for
inputs like "9:30" or variants; update toDateTime to robustly parse the time
string by splitting on ":" (or matching with a regex), normalizing each
component to two digits (pad hours/minutes and default seconds to "00" if
missing), validate numeric ranges, then construct the datetime (e.g.,
`${date}T${HH}:${MM}:${SS}`) and return null on invalid parse; reference the
toDateTime function for locating where to implement this normalization and
validation.
src/components/bookings/BookingsPage.tsx (1)

224-242: The timeout clause is redundant in this redirect guard.

useResolvedProjectSelection already derives projectBootstrapLoading as rawProjectBootstrapLoading && !projectBootstrapTimedOut, so (!projectBootstrapLoading || projectBootstrapTimedOut) simplifies to !projectBootstrapLoading. Dropping the extra term makes the redirect behavior easier to reason about.

♻️ Proposed cleanup
   useEffect(() => {
     if (
       !isTv &&
       userId &&
-      (!projectBootstrapLoading || projectBootstrapTimedOut) &&
+      !projectBootstrapLoading &&
       hasResolvedProjects &&
       !projectId &&
       typeof window !== "undefined"
     ) {
       window.location.href = "/home";
@@
   }, [
     hasResolvedProjects,
     isTv,
     projectBootstrapLoading,
-    projectBootstrapTimedOut,
     projectId,
     userId,
   ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingsPage.tsx` around lines 224 - 242, The
redirect guard in the useEffect unnecessarily repeats the timeout check: remove
the redundant projectBootstrapTimedOut term and simplify the condition to rely
only on projectBootstrapLoading (which is already computed by
useResolvedProjectSelection as rawProjectBootstrapLoading &&
!projectBootstrapTimedOut); update the effect condition inside useEffect that
currently checks "(!projectBootstrapLoading || projectBootstrapTimedOut)" to
just check "!projectBootstrapLoading" so the redirect logic uses the derived
projectBootstrapLoading flag consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/bookings/BookingsPage.tsx`:
- Around line 367-377: The summary cards render counts using authLoading ||
isLoading while the BookingList uses projectBootstrapLoading; unify them by
introducing a single surface-loading boolean (e.g., const surfaceLoading =
authLoading || isLoading || projectBootstrapLoading) in BookingsPage.tsx and
replace the ternaries that render the counts ({authLoading || isLoading ? "—" :
allBookings.length} and {authLoading || isLoading ? "—" : pendingCount}) with
checks against surfaceLoading, and use that same surfaceLoading when deciding to
show the BookingList skeleton/real list so both cards and list share the same
bootstrap loading gate.

---

Nitpick comments:
In `@src/components/bookings/BookingsPage.tsx`:
- Around line 224-242: The redirect guard in the useEffect unnecessarily repeats
the timeout check: remove the redundant projectBootstrapTimedOut term and
simplify the condition to rely only on projectBootstrapLoading (which is already
computed by useResolvedProjectSelection as rawProjectBootstrapLoading &&
!projectBootstrapTimedOut); update the effect condition inside useEffect that
currently checks "(!projectBootstrapLoading || projectBootstrapTimedOut)" to
just check "!projectBootstrapLoading" so the redirect logic uses the derived
projectBootstrapLoading flag consistently.

In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 1133-1147: The CreateBookingForm is always rendered only when
selectedActivity is truthy, so the isOpen={Boolean(selectedActivity)} prop is
redundant; update the JSX around CreateBookingForm (the conditional that checks
activityDialogMode, selectedActivity, bookingContext, and bookingContextLoading)
to either pass isOpen={true} or remove the isOpen prop entirely if
CreateBookingForm treats absence as open—adjust callers/props accordingly (look
at CreateBookingForm usage and prop interface) and ensure closeActivityFlow and
refreshLookaheadWorkspace behavior remains unchanged.
- Around line 85-90: The toDateTime function's time normalization assumes fixed
lengths and fails for inputs like "9:30" or variants; update toDateTime to
robustly parse the time string by splitting on ":" (or matching with a regex),
normalizing each component to two digits (pad hours/minutes and default seconds
to "00" if missing), validate numeric ranges, then construct the datetime (e.g.,
`${date}T${HH}:${MM}:${SS}`) and return null on invalid parse; reference the
toDateTime function for locating where to implement this normalization and
validation.
🪄 Autofix (Beta)

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

Run ID: fd94aa51-b986-4975-9725-5de8b2be7d0e

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1aacb and e03166b.

📒 Files selected for processing (9)
  • src/app/(dashboard)/assets/page.tsx
  • src/app/(dashboard)/home/page.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/multicalendar/BookingDetailDialog.tsx
  • src/components/multicalendar/MulticalendarPage.tsx
  • src/hooks/useProjectAssets.ts
  • src/hooks/useResolvedProjectSelection.ts
  • src/lib/bookingHelpers.ts
✅ Files skipped from review due to trivial changes (1)
  • src/hooks/useProjectAssets.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/multicalendar/MulticalendarPage.tsx
  • src/app/(dashboard)/assets/page.tsx
  • src/lib/bookingHelpers.ts
  • src/app/(dashboard)/home/page.tsx
  • src/hooks/useResolvedProjectSelection.ts

Comment thread src/components/bookings/BookingsPage.tsx Outdated
@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/lookahead/LookaheadDashboard.tsx (1)

325-373: ⚠️ Potential issue | 🟠 Major

Block a second upload while one is already in flight.

The button still allows a new file selection during uploading/polling, and the next startPolling() call first stopPolling()s the existing upload. That drops client-side tracking for the first upload while it is still processing server-side.

Suggested fix
+  const isUploadInFlight =
+    uploadPhase.kind === "uploading" || uploadPhase.kind === "polling";
+
   const handleFileSelected = useCallback(
     async (file: File | null) => {
-      if (!file || !projectId) return;
+      if (!file || !projectId || isUploadInFlight) return;
...
-    [projectId, startPolling],
+    [isUploadInFlight, projectId, startPolling],
   );
...
-                  disabled={!topActionButtonsReady || !projectId}
+                  disabled={
+                    !topActionButtonsReady || !projectId || isUploadInFlight
+                  }

Also applies to: 375-393, 717-733

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 325 - 373,
startPolling currently unconditionally stops any existing polling which lets a
second upload override client-side tracking; before calling stopPolling() or
scheduling a new polling interval, check the current upload phase and bail out
if an upload/polling is already active (e.g., if uploadPhase.kind is "uploading"
or "polling"/"processing"); update the startPolling(uploadId) entry guard to
return early when uploadPhase indicates an in-flight upload, and also ensure the
file-selection/upload UI uses the same uploadPhase check to disable the upload
button/input so users cannot start a second upload while polling; refer to
startPolling, stopPolling, uploadPhase, pollingRef, and isFetchingRef when
making the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 222-225: The planningCompleteness query returned by
usePlanningCompleteness is never invalidated after actions that change
readiness; update the relevant mutation handlers (the upload completion handler,
version deletion function, booking save mutation, and mapping-correction
routines) to call the React Query invalidation for that query (e.g.,
useQueryClient().invalidateQueries or queryClient.refetchQueries using the same
key/params used by usePlanningCompleteness such as ['planningCompleteness', {
projectId }]); import/useQueryClient where needed and trigger the invalidation
right after each successful mutation so the readiness pill/card reflects the
current planningCompleteness immediately.
- Around line 498-505: persistentUploadMessage is computed from
uploadStatus.completeness_notes only, but unclassifiedCount falls back to
unclassifiedMappings.length so the alert can be inconsistent; update the logic
to derive the message from the resolved unclassifiedCount instead of raw
uploadStatus.completeness_notes. Specifically, change where
persistentUploadMessage is created (and the similar blocks at the other
locations: around lines 546-555, 610-612, 898-914, 1027-1029) to call
getPersistentUploadAlertMessage with the resolved value (e.g., construct a
completeness_notes-like object or overload getPersistentUploadAlertMessage to
accept unclassifiedCount) using unclassifiedCount (which references
uploadStatus?.completeness_notes?.unclassified_mapping_count ??
unclassifiedMappings.length) so the composed alert and detail text remain
consistent.
- Around line 159-160: The dismissedAlerts state (and similarly deletingIds) is
persistent across uploads/snapshots in the same project session causing alerts
like "upload_quality" or "demand_spike" to remain hidden for later uploads; add
a useEffect that watches the snapshot and latest upload identifiers (e.g.,
snapshot.id and latestUpload.id or whatever props/vars represent the current
snapshot/upload) and calls setDismissedAlerts(new Set()) (and setDeletingIds(new
Set()) if applicable) to reset these Sets whenever the snapshot or latest upload
changes; place this effect near the useState declarations referencing
dismissedAlerts/setDismissedAlerts and deletingIds/setDeletingIds so the alert
state is cleared for new snapshots/uploads.

---

Outside diff comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 325-373: startPolling currently unconditionally stops any existing
polling which lets a second upload override client-side tracking; before calling
stopPolling() or scheduling a new polling interval, check the current upload
phase and bail out if an upload/polling is already active (e.g., if
uploadPhase.kind is "uploading" or "polling"/"processing"); update the
startPolling(uploadId) entry guard to return early when uploadPhase indicates an
in-flight upload, and also ensure the file-selection/upload UI uses the same
uploadPhase check to disable the upload button/input so users cannot start a
second upload while polling; refer to startPolling, stopPolling, uploadPhase,
pollingRef, and isFetchingRef when making the changes.
🪄 Autofix (Beta)

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

Run ID: 87c79483-ca7e-4a80-b0b7-b6f16b29b5c4

📥 Commits

Reviewing files that changed from the base of the PR and between e03166b and c1a3da2.

📒 Files selected for processing (2)
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/bookings/BookingsPage.tsx

Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx Outdated
Comment thread src/components/lookahead/LookaheadDashboard.tsx Outdated

@coderabbitai coderabbitai Bot 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 (1)
src/components/lookahead/LookaheadDashboard.tsx (1)

295-298: Consider clearing snapshot-scoped selections when the upload/snapshot changes.

This effect resets alerts and delete state, but the active drilldown/booking selection survives a new latestUploadKey or snapshotKey. If the workspace refreshes to a new snapshot mid-flow, the dialogs can keep showing stale row/activity context from the previous snapshot.

♻️ Possible follow-up
   useEffect(() => {
     setDismissedAlerts(new Set());
     setDeletingIds(new Set());
-  }, [latestUploadKey, snapshotKey]);
+    setSelectedCell(null);
+    closeActivityFlow();
+  }, [closeActivityFlow, latestUploadKey, snapshotKey]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 295 - 298, The
effect that runs on changes to latestUploadKey and snapshotKey currently clears
alerts and deletingIds but leaves snapshot-scoped selections intact; update this
effect to also clear the active drilldown/booking selection when those keys
change by calling the appropriate setters (e.g., setActiveDrilldown(null) and
setActiveBooking(null) or equivalent state-reset functions used in
LookaheadDashboard) so dialogs cannot retain stale row/activity context across
snapshot/upload refreshes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 241-264: The unclassified count is derived from
unclassifiedMappings but that query is disabled until isUploadReviewOpen,
causing unclassifiedCount to be 0 until the dialog opens; update the
useUnclassifiedMappings call (and the similar block at lines ~516-525) so its
enabled flag uses only the presence of latestVersion?.upload_id (e.g., enabled:
Boolean(latestVersion?.upload_id)) or otherwise fetch the count from
latestVersion.completeness_notes.unclassified_mapping_count before falling back
to unclassifiedMappings.length; locate the useUnclassifiedMappings hook and the
unclassifiedCount calculation and ensure the query runs when an upload exists
(not gated by isUploadReviewOpen) so the dashboard shows the correct count
without opening review.
- Around line 1128-1133: The onBook handler currently clears the drilldown
(calls setSelectedCell(null)) before bookingContext is loaded, which causes the
modal/CTA to disappear on cold or failed fetches; change onBook (and the
equivalent at 1189-1203) to first setActivityContextCell(selectedCell),
setSelectedActivity(activity) and setActivityDialogMode("booking") but do not
clear selectedCell immediately—keep it until bookingContext succeeds or an
explicit cancel occurs, and introduce a loading/error UI path in the booking
modal (driven by bookingContext presence/state) so the modal stays visible with
a spinner and a retry/close action if the context fetch fails; reference
setActivityContextCell, setSelectedActivity, setActivityDialogMode,
setSelectedCell and bookingContext when making these changes.

---

Nitpick comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 295-298: The effect that runs on changes to latestUploadKey and
snapshotKey currently clears alerts and deletingIds but leaves snapshot-scoped
selections intact; update this effect to also clear the active drilldown/booking
selection when those keys change by calling the appropriate setters (e.g.,
setActiveDrilldown(null) and setActiveBooking(null) or equivalent state-reset
functions used in LookaheadDashboard) so dialogs cannot retain stale
row/activity context across snapshot/upload refreshes.
🪄 Autofix (Beta)

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

Run ID: 0d1a6558-a002-4c8f-9979-8328fee9a7f6

📥 Commits

Reviewing files that changed from the base of the PR and between e03166b and e1a5fbe.

📒 Files selected for processing (3)
  • src/app/(dashboard)/assets/page.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/LookaheadDashboard.tsx

Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx

@coderabbitai coderabbitai Bot 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: 4

🧹 Nitpick comments (2)
src/components/bookings/BookingsPage.tsx (1)

259-268: Intentional but unconventional dependency may trigger lint warnings.

The useMemo depends on isBookingFormOpen to refresh times when the form opens, but isBookingFormOpen isn't actually read inside the callback. This will trigger an react-hooks/exhaustive-deps warning. Consider suppressing the lint rule with a comment, or use useRef to capture the form-open timestamp for clearer intent:

💡 Option 1: Add lint suppression (minimal change)
   const { nextHour, endHour } = useMemo(() => {
     const now = new Date();
     const nextHour = startOfHour(addHours(now, 1));
     return {
       nextHour,
       endHour: addHours(nextHour, 1),
     };
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally recalculate only when form opens
   }, [isBookingFormOpen]);
💡 Option 2: Clearer intent with useRef (alternative)
const formOpenedAtRef = useRef<Date | null>(null);

useEffect(() => {
  if (isBookingFormOpen) {
    formOpenedAtRef.current = new Date();
  }
}, [isBookingFormOpen]);

const { nextHour, endHour } = useMemo(() => {
  const now = formOpenedAtRef.current ?? new Date();
  const next = startOfHour(addHours(now, 1));
  return { nextHour: next, endHour: addHours(next, 1) };
}, [isBookingFormOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingsPage.tsx` around lines 259 - 268, The useMemo
for computing nextHour and endHour depends on isBookingFormOpen but does not
read it inside the callback, which triggers react-hooks/exhaustive-deps; either
suppress the lint rule with an inline comment above the useMemo
(eslint-disable-next-line react-hooks/exhaustive-deps) to keep the current
behavior, or implement a clearer approach by adding a formOpenedAtRef (useRef)
and a useEffect that sets formOpenedAtRef.current = new Date() when
isBookingFormOpen becomes true, then compute nextHour/endHour inside the
existing useMemo using formOpenedAtRef.current (falling back to new Date()) so
the dependency on isBookingFormOpen reflects the form-open timestamp intent.
src/app/(dashboard)/assets/page.tsx (1)

583-585: Reuse getAssetDisplayType() in the row renderer.

Search and sort already centralize this fallback. Using the same helper here keeps the visible type from drifting if the rule changes again.

♻️ Proposed refactor
-                        <span className="truncate capitalize">
-                          {asset.canonicalType || asset.assetType}
-                        </span>
+                        <span className="truncate capitalize">
+                          {getAssetDisplayType(asset)}
+                        </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 583 - 585, The row renderer
currently uses the inline fallback {asset.canonicalType || asset.assetType};
replace that with the shared helper getAssetDisplayType(asset) so the displayed
asset type uses the same logic used for search/sort and won't diverge if the
rule changes. Update the span content to call getAssetDisplayType and ensure the
helper is imported or available in the module where the row renderer (the
component rendering asset.canonicalType/asset.assetType) lives.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 454-477: The empty-state message must reflect the current
readinessFilter as well as searchTerm: update the empty-state rendering logic
(the block that currently uses searchTerm to show "No assets found.") to
consider readinessFilter (the same one toggled by the buttons using
setReadinessFilter and typed ReadinessFilter). Change the message branching to:
if searchTerm is present keep the "No assets found matching {searchTerm}"
behavior; otherwise switch on readinessFilter and show "No planning-ready assets
found." for 'ready', "No assets needing review found." for 'review', and the
existing "No assets found." for 'all' (or default). Ensure the message uses
readinessFilter and searchTerm variables used in this file (and leave
planningReadyCount and the filter buttons unchanged).
- Around line 253-255: When resetting pagination in the useEffect that currently
calls setCurrentPage(1) on [readinessFilter, searchTerm], clamp currentPage to
the new max page after the dataset shrinks: compute maxPages from
filteredAndSortedAssets.length and pageSize and call setCurrentPage(prev =>
Math.min(prev, maxPages || 1)) (or set to 1 if empty). Do this before the
component's early return so the UI never shows "Page X of Y" where X > Y, and
remove the later redundant totalPages declaration to avoid conflicting
calculations; reference the useEffect, setCurrentPage, currentPage,
filteredAndSortedAssets and totalPages identifiers when making the change.

In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 85-111: The toDateTime function lets empty time parts like "" be
treated as 0 because Number("") === 0; fix by validating the raw time parts
before numeric conversion: in toDateTime, ensure time.trim().split(":") returns
2 or 3 parts and that hoursRaw and minutesRaw are non-empty and consist only of
digits (and if secondsRaw is present it must also be non-empty and digits) (use
a simple /^\d+$/ check), return null for any failure, then convert to numbers
and continue with the existing range checks and normalizedTime/Date parsing.
This change localizes to the toDateTime function.
- Around line 669-671: The Tailwind classes used in LookaheadDashboard.tsx
(e.g., "max-w-screen", "rounded-7", "h-90", "min-w-70", "max-h-75" found in the
className on the outer divs) are not standard — add corresponding theme.extend
entries to your Tailwind config (tailwind.config.ts) to define maxWidth,
borderRadius, height, minWidth and maxHeight keys with the desired values so
those utility classes resolve correctly at build time.

---

Nitpick comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 583-585: The row renderer currently uses the inline fallback
{asset.canonicalType || asset.assetType}; replace that with the shared helper
getAssetDisplayType(asset) so the displayed asset type uses the same logic used
for search/sort and won't diverge if the rule changes. Update the span content
to call getAssetDisplayType and ensure the helper is imported or available in
the module where the row renderer (the component rendering
asset.canonicalType/asset.assetType) lives.

In `@src/components/bookings/BookingsPage.tsx`:
- Around line 259-268: The useMemo for computing nextHour and endHour depends on
isBookingFormOpen but does not read it inside the callback, which triggers
react-hooks/exhaustive-deps; either suppress the lint rule with an inline
comment above the useMemo (eslint-disable-next-line react-hooks/exhaustive-deps)
to keep the current behavior, or implement a clearer approach by adding a
formOpenedAtRef (useRef) and a useEffect that sets formOpenedAtRef.current = new
Date() when isBookingFormOpen becomes true, then compute nextHour/endHour inside
the existing useMemo using formOpenedAtRef.current (falling back to new Date())
so the dependency on isBookingFormOpen reflects the form-open timestamp intent.
🪄 Autofix (Beta)

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

Run ID: 84496f61-a221-4319-8f83-9cd2e6e000ff

📥 Commits

Reviewing files that changed from the base of the PR and between e03166b and bd5f28f.

📒 Files selected for processing (4)
  • src/app/(dashboard)/assets/page.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/ActivityDrilldownDialog.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/lookahead/ActivityDrilldownDialog.tsx

Comment thread src/app/(dashboard)/assets/page.tsx Outdated
Comment thread src/app/(dashboard)/assets/page.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx

@coderabbitai coderabbitai Bot 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: 3

🧹 Nitpick comments (2)
src/components/bookings/BookingsPage.tsx (1)

164-169: Avoid trimming booking.source twice.

This works, but you can simplify and avoid duplicate trim() calls for readability.

💡 Optional cleanup
+  const normalizedSource = booking.source?.trim();
+
   return {
@@
-    bookingSource: booking.source?.trim() ? booking.source.trim() : null,
+    bookingSource: normalizedSource ? normalizedSource : null,
     bookingGroupId: booking.booking_group_id ?? null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/bookings/BookingsPage.tsx` around lines 164 - 169, The
bookingSource assignment trims booking.source twice; change it to compute the
trimmed value once and reuse it when setting bookingSource (e.g., derive a
single const like trimmedSource = booking.source?.trim() and then set
bookingSource: trimmedSource ?? null) so BookingPage's bookingSource property
avoids duplicate trim() calls and is clearer.
src/app/(dashboard)/assets/page.tsx (1)

474-492: Filter buttons are well-implemented with proper accessibility.

Good use of aria-pressed for screen reader support. The type cast on line 483 works, though using as const on the array would provide automatic type inference:

✨ Optional: Use `as const` for type-safe mapping
-                {[
-                  ["all", "All assets"],
-                  ["ready", "Planning ready"],
-                  ["review", "Needs review"],
-                ].map(([value, label]) => (
+                {([
+                  ["all", "All assets"],
+                  ["ready", "Planning ready"],
+                  ["review", "Needs review"],
+                ] as const).map(([value, label]) => (
                   <button
                     key={value}
                     type="button"
                     aria-pressed={readinessFilter === value}
-                    onClick={() => setReadinessFilter(value as ReadinessFilter)}
+                    onClick={() => setReadinessFilter(value)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 474 - 492, Replace the
inline array with a const tuple typed via "as const" so TypeScript infers the
exact string literals and you can remove the manual cast in the onClick; e.g.
create a const like options = [["all","All assets"],["ready","Planning
ready"],["review","Needs review"]] as const, then map over options and call
setReadinessFilter(value) (where value is inferred as the union matching
ReadinessFilter), keeping existing identifiers readinessFilter and
setReadinessFilter and preserving the aria-pressed and className logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 815-818: The banner retry handler onUploadAnother doesn't clear
the hidden file input value so choosing the same file can noop; update the
onUploadAnother path (where setUploadPhase is called) to also reset the input by
checking fileInputRef.current and setting its value to an empty string before
calling fileInputRef.current.click(), e.g. null-safe clear of
fileInputRef.current.value then trigger click so the input's onChange will fire
for the same file selection.
- Around line 205-209: The current useEffect in LookaheadDashboard blindly casts
persisted?.windowSize to WindowSize and restores it via setWindowSizeLocal,
which can introduce invalid values; update the effect to validate
persisted.windowSize against the allowed WindowSize options (e.g., check
membership in the keys of WINDOW_WEEKS or the WindowSize enum) before calling
setWindowSizeLocal, and if invalid or missing fall back to the default "4W";
reference useEffect, hasUIIntentHydrated, uiScopeKey,
useUIIntentStore.getState().getLookaheadIntent, setWindowSizeLocal,
WINDOW_WEEKS, and WindowSize when making this change.
- Around line 1211-1227: Handlers onCorrectMapping and onPromoteToMemory only
refresh upload-review data; after calling
updateProgrammeMapping/promoteItemClassification and the existing
Promise.allSettled of mutateMappings(), mutateUnclassified(),
mutateUploadStatus(), mutatePlanningCompleteness(), also trigger a dashboard
revalidation by calling the dashboard revalidation function (e.g.,
mutateDashboardSurface() or mutateWorkspace()) so the main
heatmap/coverage/readiness UI updates immediately; add that mutate call (or
include it inside the Promise.allSettled array) in both onCorrectMapping and
onPromoteToMemory.

---

Nitpick comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 474-492: Replace the inline array with a const tuple typed via "as
const" so TypeScript infers the exact string literals and you can remove the
manual cast in the onClick; e.g. create a const like options = [["all","All
assets"],["ready","Planning ready"],["review","Needs review"]] as const, then
map over options and call setReadinessFilter(value) (where value is inferred as
the union matching ReadinessFilter), keeping existing identifiers
readinessFilter and setReadinessFilter and preserving the aria-pressed and
className logic.

In `@src/components/bookings/BookingsPage.tsx`:
- Around line 164-169: The bookingSource assignment trims booking.source twice;
change it to compute the trimmed value once and reuse it when setting
bookingSource (e.g., derive a single const like trimmedSource =
booking.source?.trim() and then set bookingSource: trimmedSource ?? null) so
BookingPage's bookingSource property avoids duplicate trim() calls and is
clearer.
🪄 Autofix (Beta)

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

Run ID: 458f66be-aa85-473f-8caf-ebe791ada717

📥 Commits

Reviewing files that changed from the base of the PR and between e03166b and da907dc.

📒 Files selected for processing (4)
  • src/app/(dashboard)/assets/page.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/ActivityDrilldownDialog.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/lookahead/ActivityDrilldownDialog.tsx

Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/app/(dashboard)/assets/page.tsx (1)

909-911: Consider clamping confidence display values.

The percentage calculation assumes typeInferenceConfidence is in the 0–1 range. If the backend ever sends a value outside this range (e.g., already as a percentage), the display could show unexpected results like "150%".

💡 Optional defensive fix
 {selectedAsset.typeInferenceConfidence != null
-  ? `${Math.round(selectedAsset.typeInferenceConfidence * 100)}%`
+  ? `${Math.min(100, Math.max(0, Math.round(selectedAsset.typeInferenceConfidence * 100)))}%`
   : "n/a"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/assets/page.tsx around lines 909 - 911, The displayed
confidence can exceed 100% if the backend returns values outside 0-1; update the
rendering for selectedAsset.typeInferenceConfidence to compute a clamped
percentage first (convert values <=1 by multiplying by 100, treat values >1 as
already percentage), then clamp the result to the 0–100 range and round for
display, and use that safePercent in place of the current inline expression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`(dashboard)/assets/page.tsx:
- Around line 909-911: The displayed confidence can exceed 100% if the backend
returns values outside 0-1; update the rendering for
selectedAsset.typeInferenceConfidence to compute a clamped percentage first
(convert values <=1 by multiplying by 100, treat values >1 as already
percentage), then clamp the result to the 0–100 range and round for display, and
use that safePercent in place of the current inline expression.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e36fbd85-e84c-4adc-9a83-015ca99a1a3c

📥 Commits

Reviewing files that changed from the base of the PR and between e03166b and fad0e04.

📒 Files selected for processing (4)
  • src/app/(dashboard)/assets/page.tsx
  • src/components/bookings/BookingsPage.tsx
  • src/components/lookahead/ActivityDrilldownDialog.tsx
  • src/components/lookahead/LookaheadDashboard.tsx

@Ross1116 Ross1116 merged commit fa8f469 into main Mar 27, 2026
3 checks passed
Sign up for free to 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