Todos Two-Pane Planner — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Replace the current Todos page (MyTodoListPage) with a two-pane “planner”: a left rail of todos grouped by due-date bucket beside a reused week-appointments calendar, with due-dated todos shown as all-day chips — reusing existing data, CRUD, and the WeekViewV2 calendar, with zero DB schema change.
Architecture: A new TodoPlannerPage keeps MyTodoListPage’s data layer verbatim (same useAuth, ['doctor-todos'] query, three mutations, and the showForm→TodoFormPage branch) and swaps the list body for <TodoRail> + <TodoWeekCalendar>. Bucketing is a pure, unit-tested function. The calendar reuses SchedulerProvider + WeekViewV2 read-only (week view has no drag), seeded by the existing getAppointments API and a small extracted mapAppointmentsToEvents helper. WeekViewV2 gets one additive, optional prop (renderAllDayCell) for the chip strip — omitted by the Appointments calendar, so it is unaffected.
Tech Stack: React 19, TypeScript, Vite, TanStack Query v5, react-router-dom v7, date-fns v4, Radix/shadcn UI primitives, vitest + Testing Library, lucide-react.
File structure
New:ui/src/components/todos/lib/bucketTodos.ts— pure bucketing + default-due-date logic.ui/src/components/todos/lib/bucketTodos.test.ts— vitest unit tests.ui/src/lib/appointments-to-events.ts— pureAppointment[] → Event[]mapper (shared).ui/src/lib/appointments-to-events.test.ts— vitest unit test.ui/src/components/todos/planner/TodoRailRow.tsx— one todo row (checkbox + title + meta + hover actions).ui/src/components/todos/planner/TodoBucket.tsx— collapsible bucket section + inline quick-add.ui/src/components/todos/planner/TodoRail.tsx— dark rail (header, search, buckets, done footer).ui/src/components/todos/planner/TodoWeekCalendar.tsx— right pane (week header, provider, WeekViewV2, all-day chips).ui/src/components/todos/TodoPlannerPage.tsx— orchestrator (data, URL state, responsive split, form branch).
ui/src/components/schedule/_components/view/week/week-view-v2.tsx— add optionalrenderAllDayCellprop (additive only).ui/src/components/dashboards/DoctorDashboard.tsx— renderTodoPlannerPageforactiveView==='todos'.ui/src/components/dashboards/AdminDashboard.tsx— same.ui/src/lib/nav-registry.ts— addtodosentry toMODULE_PARAMS.
permissions.ts (client+server), clinic_modules, serverComm todo CRUD, TodoFormPage, AppointmentCalendar.tsx.
Commands: typecheck cd ui && npx tsc --noEmit; tests cd ui && npx vitest run <path>; build cd ui && npm run build; dev cd ui && npm run dev.
Task 1: Pure bucketing logic (bucketTodos)
Files:
- Create:
ui/src/components/todos/lib/bucketTodos.ts - Test:
ui/src/components/todos/lib/bucketTodos.test.ts
MyTodoListPage’s existing new Date(dueAt)/isPast behavior and format(new Date(dueAt),'d MMM yyyy') display; clinic users are in PKT/UTC+5 and dueAt is stored at noon UTC, so local interpretation matches the displayed date). Week starts Monday to match WeekViewV2.
- Step 1: Write the failing test
- Step 2: Run test to verify it fails
cd ui && npx vitest run src/components/todos/lib/bucketTodos.test.ts
Expected: FAIL — Cannot find module './bucketTodos'.
- Step 3: Write the implementation
- Step 4: Run test to verify it passes
cd ui && npx vitest run src/components/todos/lib/bucketTodos.test.ts
Expected: PASS (8 tests).
- Step 5: Commit
Task 2: Shared mapAppointmentsToEvents helper
Extract the exact mapping from AppointmentCalendar.tsx:442-480 into a reusable pure function (the new calendar uses it; AppointmentCalendar is left untouched to avoid risk — consolidating it onto this helper is a future cleanup).
Files:
-
Create:
ui/src/lib/appointments-to-events.ts -
Test:
ui/src/lib/appointments-to-events.test.ts - Step 1: Write the failing test
- Step 2: Run test to verify it fails
cd ui && npx vitest run src/lib/appointments-to-events.test.ts
Expected: FAIL — module not found.
- Step 3: Write the implementation (copied verbatim from
AppointmentCalendar.tsx:442-480, minus the highlight wrapper)
- Step 4: Run test to verify it passes
cd ui && npx vitest run src/lib/appointments-to-events.test.ts
Expected: PASS. If TS complains a property doesn’t exist on Appointment/Event, drop that property (match the real types in @/types/index and serverComm); the test only asserts the core fields.
- Step 5: Commit
Task 3: Additive renderAllDayCell prop on WeekViewV2
The new calendar needs a per-day all-day strip aligned to the grid columns. Add one optional prop; when omitted (the Appointments calendar), the component renders identically to today.
Files:
-
Modify:
ui/src/components/schedule/_components/view/week/week-view-v2.tsx - Step 1: Extend the Props interface (lines 21-27)
- Step 2: Destructure the prop (line 53)
export default function WeekViewV2({ currentDate: propDate, onDateChange, startHour = 9, endHour = 18, hideHeader }: Props) {
with export default function WeekViewV2({ currentDate: propDate, onDateChange, startHour = 9, endHour = 18, hideHeader, renderAllDayCell }: Props) {
- Step 3: Render the all-day row — insert immediately after the day-header
visibleDays.map(...)block closes (right after line 120’s})}and before the{/* Hour rail */}comment on line 122)
- Step 4: Typecheck
cd ui && npx tsc --noEmit
Expected: no new errors. (React is already imported at line 1; localDateStr at line 9.)
- Step 5: Visual regression check (Appointments unaffected) — REQUIRED (risk R2)
cd ui && npm run dev, open the Appointments calendar, switch to Week view in both light and dark mode. Confirm it looks identical to before (no all-day row, columns aligned). Take screenshots. The Appointments calendar never passes renderAllDayCell, so the new branch must not render.
- Step 6: Commit
Task 4: TodoRailRow component
A compact todo row for the dark rail. Adapts TodoRow (MyTodoListPage.tsx:298-388) to the rail’s tighter layout; reuses Checkbox, Badge, lucide icons. Done/overdue styling preserved.
Files:
-
Create:
ui/src/components/todos/planner/TodoRailRow.tsx - Step 1: Create the component
- Step 2: Typecheck
cd ui && npx tsc --noEmit
Expected: no errors.
- Step 3: Commit
Task 5: TodoBucket (collapsible section + quick-add)
Files:
-
Create:
ui/src/components/todos/planner/TodoBucket.tsx - Step 1: Create the component
- Step 2: Typecheck
cd ui && npx tsc --noEmit
Expected: no errors.
- Step 3: Commit
Task 6: TodoRail (compose buckets)
Files:
-
Create:
ui/src/components/todos/planner/TodoRail.tsx - Step 1: Create the component
- Step 2: Typecheck
cd ui && npx tsc --noEmit
Expected: no errors.
- Step 3: Commit
Task 7: TodoWeekCalendar (right pane)
Fetches the visible week’s appointments, maps them with mapAppointmentsToEvents, seeds a SchedulerProvider, and renders WeekViewV2 with the all-day chip strip. For doctors, pre-filters events to their own appointments so the calendar reflects their schedule; admins see all.
Files:
-
Create:
ui/src/components/todos/planner/TodoWeekCalendar.tsx - Step 1: Create the component
Note:SchedulerProvider’sinitialStateseeds events once; becauseweekDate/eventschange the component re-renders and the provider re-seeds oninitialStatechange (as inAppointmentCalendar, whereeventsstate drives it). If events don’t refresh on week change during the visual pass, set akey={startStr}on<SchedulerProvider>to force a clean re-seed per week.
- Step 2: Typecheck
cd ui && npx tsc --noEmit
Expected: no errors. If qk.appointments.list rejects the scope: 'todos-planner' literal, widen by using the same shape the calendar uses or qk.appointments.all() as the key.
- Step 3: Commit
Task 8: TodoPlannerPage (orchestrator)
Keeps MyTodoListPage’s data layer verbatim; swaps the list body for the two-pane layout; adds ?date week state; collapses to rail-only with a “Calendar” toggle below lg.
Files:
-
Create:
ui/src/components/todos/TodoPlannerPage.tsx - Step 1: Create the component
Note:useDeviceTypeis called after the earlyshowFormreturn. Move bothuseDeviceType()anduseStatecalls above theif (showForm)return to satisfy the rules-of-hooks (hooks must run unconditionally). Place them right afterediting/handleFormSubmitand before theif (showForm)block.
-
Step 2: Fix hook ordering — move
const { isMobile, isTablet } = useDeviceType();andconst [mobilePane, setMobilePane] = useState<'list'|'calendar'>('list');to aboveif (showForm). - Step 3: Typecheck
cd ui && npx tsc --noEmit
Expected: no errors. Confirm createDoctorTodo({ body, dueAt }) matches its signature (body required, dueAt?: string|null). Confirm useDeviceType is exported from @/components/ui/use-mobile (used by WeekViewV2).
- Step 4: Commit
Task 9: Wire into dashboards + nav-registry
Files:-
Modify:
ui/src/components/dashboards/DoctorDashboard.tsx(line 42 import, line 233 render) -
Modify:
ui/src/components/dashboards/AdminDashboard.tsx(line 40 import, line 301 render) -
Modify:
ui/src/lib/nav-registry.ts(MODULE_PARAMS) - Step 1: Swap the lazy import in both dashboards
DoctorDashboard.tsx replace:
AdminDashboard.tsx (import line 40, render line 301).
- Step 2: Register
todosparams innav-registry.ts— add insideMODULE_PARAMS(e.g. after theappointmentsline):
- Step 3: Typecheck + build
cd ui && npx tsc --noEmit && npm run build
Expected: clean typecheck; build succeeds.
- Step 4: Commit
Task 10: Verification & visual pass (REQUIRED)
Files: none (verification only).- Step 1: Full typecheck, tests, build
cd ui && npx tsc --noEmit && npx vitest run src/components/todos/lib/bucketTodos.test.ts src/lib/appointments-to-events.test.ts && npm run build
Expected: all pass.
- Step 2: Run the app and verify (light + dark)
cd ui && npm run dev, sign in to the canonical test tenant “ssh & Associates” (clinic b6d3a3f3-…). Navigate to ?view=todos. Verify:
- Dark rail shows buckets (Overdue/This week/This month/Later/No due date) with counts; pinned float to top; overdue is red.
- Checkbox toggles done (moves to Done footer); pin/archive hover actions work.
-
+ Todoquick-add in a bucket creates a todo with the right due date and it lands in that bucket. -
Clicking a row opens
TodoFormPage; back returns to the planner. -
Right pane shows the week’s appointments (doctor sees only their own; admin sees all), correct week header
Month YYYY / W##, prev/next/Today work and update?date. - Due-todo chips appear in the correct day’s all-day strip and open the form on click.
-
Resize below
lg: rail-only with a working Todos/Calendar toggle. Capture screenshots (light + dark). - Step 3: Appointments regression check (risk R2)
- Step 4: Legacy URL check
/doctor/todos → confirm it still redirects to /dashboard?view=todos and renders the planner.
- Step 5 (optional cleanup): remove the old page once parity confirmed
MyTodoListPage has no remaining importers (grep -rn "MyTodoListPage" ui/src), delete ui/src/components/todos/MyTodoListPage.tsx. Keep TodoFormPage, TodoVoiceRecorder, AppointmentTodoPanel. Commit separately:
Self-review notes
- Spec coverage: layout (Tasks 6–8), calendar reuse + read-only (Task 7; week view has no drag), all-day chips (Tasks 3+7), bucketing PKT/local + Monday week (Task 1), access unchanged (Task 8 keeps
useAuth/isAdmin; doctor calendar pre-filter is an additive relevance improvement, not an access change), responsive (Task 8), URL/MODULE_PARAMS(Tasks 8–9), no schema change (R1 resolved: quick-add sendsbody), regression guard (Tasks 3/10). All covered. - R1 (body required): resolved —
quickAddMutand the chip path sendbody, never a bare title. - R3 (timezone): bucketing uses local date interpretation, matching the existing
isPast/formatdisplay; documented assumption (PKT users, noon-UTCdueAt). - R5 (read-only calendar): week view is inherently non-mutating (drag is day-view only; click dispatches a global event no one listens to in this module).
- Type consistency:
BucketKey,bucketTodos,bucketDefaultDueIso,todosDueOn,mapAppointmentsToEvents,TodoRailRow/TodoBucket/TodoRail/TodoWeekCalendarprops are consistent across tasks. - Hooks: Task 8 Step 2 fixes conditional-hook ordering explicitly.

