Skip to main content

Todos — Two-Pane Planner Redesign

Date: 2026-05-31 Status: Approved (design), proceeding to implementation plan Owner: ssh

1. Summary

Redesign the existing Todos page into a two-pane “planner” inspired by the Cal.com/Routine-style reference: a dark left rail listing todos grouped by due-date bucket, beside a light week calendar showing the user’s real appointments with due-dated todos surfaced as all-day chips. The redesign replaces the current Todos page UI in place (same nav slot, same ?view=todos URL). It is not nested under Appointments in v1 (the user chose “replace in place”); nesting under Appointments is a trivial, optional follow-up (Finance children[] pattern) and is explicitly deferred. Guiding constraints (from the user): reuse existing components, “no complex stuff,” speed and precision. Therefore: zero database schema change, reuse WeekViewV2/SchedulerProvider and TodoFormPage as-is, and no drag-and-drop in v1.

2. Decisions (locked)

DecisionChoice
Calendar pane contentAppointments calendar + todo all-day chips (reuse WeekViewV2; todos with due dates this week shown as all-day chips). Zero schema change.
Access / rolesDoctors + Admins — unchanged from today. Doctors see only their own todos; clinic admins audit all in the clinic. Receptionists/patients remain blocked at the route layer.
Module nameTodos (unchanged)
Nav placementReplace current Todos page in place — same nav item, same ?view=todos view id. (Nesting under Appointments deferred / optional.)
Interactions (v1)Reuse-only: inline checkbox to complete, per-bucket + Todo quick-add row, click a todo to open the existing full-page TodoFormPage. No drag-and-drop.

3. Existing assets to reuse (verified by exploration)

Todos (no schema change)

  • server/src/schema/doctor_todos.tsdoctorTodos table. Columns: id, clinicId, createdBy, title, body, voiceLang, linkedKind, linkedId, status (open|done|archived), pinned, dueAt (timestamp, used date-only), assignedTo, tags (jsonb), createdAt, updatedAt.
  • server/src/routes/doctor-todos.tsGET /todos (status/limit; admin sees all in clinic, doctors own only), GET /todos/by-case, POST /, PATCH /:id, DELETE /:id (soft-delete → status='archived'). notifyTodoAssignee() fires persistent in-app notification on (re)assignment.
  • ui/src/lib/serverComm.tslistDoctorTodos(), createDoctorTodo(), updateDoctorTodo(), deleteDoctorTodo(); types DoctorTodo, DoctorTodoStatus, DoctorTodoLinkedKind. Base: /api/v1/protected/todos.
  • ui/src/components/todos/MyTodoListPage.tsxto be replaced by TodoPlannerPage. Contains the current TodoRow, tab filtering, search, mutation orchestration, overdue detection (isPast(dueAt)), and URL state (?todoTab=, ?q=, ?action=new, ?id=).
  • ui/src/components/todos/TodoFormPage.tsxreused as-is for create/edit (opened via ?action=new / ?id=).
  • ui/src/components/todos/TodoVoiceRecorder.tsx, AppointmentTodoPanel.tsx — unchanged.

Appointments calendar (reused, read-only in this module)

  • ui/src/components/schedule/_components/view/week/week-view-v2.tsxWeekViewV2: 7-column Monday-start time grid, responsive (1/3/7 cols), absolute event positioning top = ((startMin/60) - startHour) * pxPerHour. Critical shared component — any change must be additive + optional and must not regress the Appointments view.
  • ui/src/providers/schedular-provider.tsxSchedulerProvider context (events, doctorSchedules, operatingHours).
  • ui/src/components/schedule/_components/view/schedular-view-filteration.tsx — date nav (prev/next), getWeekNumber (ISO 8601 → “W22”), date title, doctor/status filters.
  • ui/src/components/appointments/AppointmentCard.tsxDOCTOR_COLORS (10 soft pastels). ui/src/components/schedule/_components/v2/EventCard.tsx, status-style.ts, NowLine.tsx, use-day-layout.ts, use-day-index.ts.
  • ui/src/components/appointments/AppointmentCalendar.tsx — reference for how the provider is seeded and the getAppointments(startDate, endDate) fetch + CustomEventAdapter mapping works.
  • server/src/schema/appointments.tsappointmentDate (DATE), appointmentTime (TIME), durationMinutes, status enum.

Design system

  • Tokens: shared/design-tokens.ts + CSS vars in ui/src/index.css (:root light / .dark dark, Tailwind v4 @theme inline). Dark mode = .dark class. Fonts Poppins/Inter. Radius base 12px.
  • Primitives in ui/src/components/ui/: card, button, badge, checkbox, input, dialog, avatar, tabs, accordion, collapsible, tooltip, dropdown-menu, separator, scroll-area, select, textarea, label, skeleton.
  • Layout shell: ui/src/components/layout/AppLayout.tsx. Sidebar bg light hsl(210 40% 98%) / dark hsl(240 10% 5%).
  • Icons: lucide-react only. Never DollarSign/BadgeDollarSign → use Banknote. AI surfaces only branded “Ruby” (N/A here).

Routing / dashboards

  • Query-param views (?view=todos). Rendered where activeView === 'todos': ui/src/components/dashboards/DoctorDashboard.tsx (and any other dashboard that renders todos — verify Admin/Receptionist).
  • ui/src/lib/nav-registry.tsMODULE_PARAMS (params each view owns; cleaned on view switch).
  • ui/src/App.tsx — legacy redirect /doctor/todos → /dashboard?view=todos.

4. Architecture

New component ui/src/components/todos/TodoPlannerPage.tsx replaces MyTodoListPage at every render site. It composes three focused units:
TodoPlannerPage  (orchestrator: data fetch, URL state, responsive split)
├── TodoRail            (left dark rail)
│   ├── TodoBucketSection  (collapsible, count badge)  — reuses ui/collapsible
│   │   ├── TodoRailRow    (checkbox + title + meta)   — reuses ui/checkbox, ui/avatar, ui/badge
│   │   └── QuickAddRow    (inline "+ Todo")
│   └── DoneFooter         (collapsed "Done this week")
├── TodoWeekCalendar    (right pane)
│   ├── <SchedulerProvider> seeded with week appointments
│   │   └── <WeekViewV2 readOnly />   — reused, read-only
│   └── TodoAllDayStrip   (todo chips per day column)
└── (TodoFormPage opened via ?action=new / ?id=)   — reused as-is

4.1 Bucketing (pure client logic) — ui/src/components/todos/lib/bucketTodos.ts

Input: DoctorTodo[] + “today” (PKT). Output keyed buckets, each sorted pinned-first then by dueAt/createdAt:
  • Overduestatus==='open' and dueAt calendar-date < today (red styling, reuse isPast semantics).
  • This week — open, dueAt within current ISO week (Mon–Sun) and ≥ today.
  • This month — open, dueAt within current calendar month but after this week.
  • Later — open, dueAt beyond this month.
  • No due date — open, dueAt == null.
  • Donestatus==='done' (collapsed footer; archived excluded).
Date math is PKT-aware and Monday-week aligned to match WeekViewV2 + getWeekNumber. dueAt is stored at 12:00:00Z (noon) so calendar-date comparison is timezone-stable; reuse existing localDateStr/PKT helpers (pkt.ts). Empty buckets are hidden.

4.2 Right pane — week calendar

  • Fetch the visible week’s appointments via the existing getAppointments(startDate, endDate) and seed a SchedulerProvider, mirroring AppointmentCalendar.tsx’s adapter.
  • Render WeekViewV2 in read-only mode (no drag reschedule, no create-on-click). Clicking an appointment deep-links to the real Appointments view (?view=appointments&appointmentId=…&date=…).
  • Week view only (no day/month switcher in this module) to match the reference. Reuse prev/next/Today + getWeekNumber for the header (Month YYYY / W##).

4.3 All-day todo chips

Surface open todos whose dueAt falls on a visible day as a chip in a thin all-day strip aligned to the 7 day columns. Clicking a chip opens the todo (?id=); the chip reflects done state. Implementation preference (least-risk first):
  1. Preferred: add an optional render prop to WeekViewV2, e.g. renderDayBanner?: (date: Date) => ReactNode, rendered in the existing day-header band. Appointments usage omits it → zero behavior change. Planner passes it to inject chips. Must be verified visually against the Appointments calendar (no regression).
  2. Fallback (if the component structure makes the prop awkward): render the chip strip as a sibling row in TodoWeekCalendar, with column widths matched to WeekViewV2’s gutter + 7 equal columns.
The implementer picks (1) or (2) after reading week-view-v2.tsx; the bar is no Appointments regression.

4.4 Interactions (reuse-only)

  • Complete: rail checkbox → optimistic updateDoctorTodo({status}), re-bucket, refresh chip.
  • Quick-add: per-bucket inline input; Enter → createDoctorTodo({ title, dueAt: bucketDefaultDate }) (This week→today; This month→first day of next-week-in-month or today; No due datenull). See Risk R1 re: body requirement.
  • Open/edit: click row/chip → existing TodoFormPage via URL state.
  • Pin/archive: keep the existing hover actions from TodoRow (pin floats within bucket; archive soft-deletes).

4.5 Responsive

  • ≥ lg: two-pane (rail ~288px + calendar fills).
  • < lg: rail is primary; a “Calendar” toggle swaps to the week view full-width. Reuse useDeviceType.

4.6 URL state & nav

  • Keep ?view=todos. Reuse/extend URL params: ?q= (search), ?action=new, ?id= (form), ?date= (week anchor). Update MODULE_PARAMS['todos'] in nav-registry.ts to own date (and keep q, action, id) so they aren’t stripped on view switch. ?todoTab is retired (buckets replace tabs).
  • Nav item, label (“Todos”), permissions: unchanged. No edits to permissions.ts (client or server) or clinic_modules.

5. Data flow

  1. On mount / week change: fetch todos (listDoctorTodos, admin→all, doctor→own) and the week’s appointments (getAppointments).
  2. Bucket todos client-side (PKT). Render rail.
  3. Seed SchedulerProvider with appointments; render read-only WeekViewV2; overlay all-day chips from due-dated todos.
  4. Mutations (complete/quick-add/pin/archive/edit) optimistically update local todo list → re-bucket + re-derive chips. Reuse existing mutation calls; no new endpoints.

6. Out of scope (v1)

  • Drag-and-drop (todo→day, reorder).
  • Time-of-day on todos / scheduling todos at specific hours (data is date-only).
  • User-defined lists (the reference’s “Personal”/“Books to read”); buckets are due-date based. (Tag-based grouping is a possible future enhancement — existing tags array.)
  • Nesting under Appointments in the nav (trivial follow-up; deferred).
  • TanStack Query migration (keep existing fetch/mutation patterns; out of scope).
  • Sharing / share-links (the reference’s “Share with” popover).

7. Risks & mitigations

  • R1 — Quick-add body requirement. The edit form treats body/description as required. Verify whether POST /todos server validation requires body. If it does: either relax server validation to allow title-only, or have quick-add send an empty/placeholder body. Must confirm before wiring quick-add.
  • R2 — WeekViewV2 regression. It’s a shared, critical component. Any change must be additive + optional and verified with a visual pass on the Appointments calendar (light + dark) before completion.
  • R3 — Date/timezone correctness. Bucketing and “today”/overdue must be PKT-aware and Monday-week aligned to match the calendar; reuse localDateStr/pkt.ts. Avoid toISOString() day-boundary bugs.
  • R4 — Admin “sees all” noise. Admins audit all clinic todos; the rail could be long. Provide the existing assignee/creator filter (reuse) so admins can narrow; default to a sensible scope.
  • R5 — Read-only calendar. Ensure WeekViewV2 in this module does not allow reschedule/create (could mutate real appointments). Gate via a readOnly prop or wrapper.
  • R6 — Render-site coverage. MyTodoListPage may be rendered by more than one dashboard. Swap all activeView==='todos' render sites to TodoPlannerPage. Remove MyTodoListPage only after parity is confirmed (extract reusable bits like TodoRow meta first).

8. Testing / verification

  • Visual pass (reload + screenshot, light + dark) of: the planner (rail buckets, quick-add, complete, chips) and the unchanged Appointments calendar (regression check for R2).
  • Verify against the canonical test tenant “ssh & Associates” (clinicId b6d3a3f3-…): real doctor schedules, appointments, and todos.
  • Doctor vs Admin visibility: doctor sees own only; admin sees all (with filter).
  • Empty states: no todos, no appointments, empty buckets hidden.
  • ?view=todos and legacy /doctor/todos still land on the planner.
  • tsc/build is necessary but not sufficient — UI quality requires the visual pass.

9. File-change map (anticipated)

New:
  • ui/src/components/todos/TodoPlannerPage.tsx
  • ui/src/components/todos/_planner/TodoRail.tsx, TodoBucketSection.tsx, TodoRailRow.tsx, QuickAddRow.tsx, TodoWeekCalendar.tsx, TodoAllDayStrip.tsx
  • ui/src/components/todos/lib/bucketTodos.ts (+ unit-testable date logic)
Edited:
  • Dashboard render sites of MyTodoListPageTodoPlannerPage (DoctorDashboard.tsx, and Admin/Receptionist if applicable).
  • ui/src/lib/nav-registry.tsMODULE_PARAMS['todos'] add date.
  • Possibly ui/src/components/schedule/_components/view/week/week-view-v2.tsx — optional renderDayBanner prop + readOnly (additive only).
  • server/src/routes/doctor-todos.ts — only if R1 requires relaxing body validation.
Removed (after parity):
  • ui/src/components/todos/MyTodoListPage.tsx (extract reusable row meta first).
Unchanged: all DB schema/migrations, permissions.ts (client+server), clinic_modules, serverComm todo CRUD, TodoFormPage.