Todos — Two-Pane Planner Redesign
Date: 2026-05-31 Status: Approved (design), proceeding to implementation plan Owner: ssh1. 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)
| Decision | Choice |
|---|---|
| Calendar pane content | Appointments calendar + todo all-day chips (reuse WeekViewV2; todos with due dates this week shown as all-day chips). Zero schema change. |
| Access / roles | Doctors + 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 name | Todos (unchanged) |
| Nav placement | Replace 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.ts—doctorTodostable. 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.ts—GET /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.ts—listDoctorTodos(),createDoctorTodo(),updateDoctorTodo(),deleteDoctorTodo(); typesDoctorTodo,DoctorTodoStatus,DoctorTodoLinkedKind. Base:/api/v1/protected/todos.ui/src/components/todos/MyTodoListPage.tsx— to be replaced byTodoPlannerPage. Contains the currentTodoRow, tab filtering, search, mutation orchestration, overdue detection (isPast(dueAt)), and URL state (?todoTab=, ?q=, ?action=new, ?id=).ui/src/components/todos/TodoFormPage.tsx— reused 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.tsx—WeekViewV2: 7-column Monday-start time grid, responsive (1/3/7 cols), absolute event positioningtop = ((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.tsx—SchedulerProvidercontext (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.tsx—DOCTOR_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 thegetAppointments(startDate, endDate)fetch +CustomEventAdaptermapping works.server/src/schema/appointments.ts—appointmentDate (DATE),appointmentTime (TIME),durationMinutes,statusenum.
Design system
- Tokens:
shared/design-tokens.ts+ CSS vars inui/src/index.css(:rootlight /.darkdark, Tailwind v4@theme inline). Dark mode =.darkclass. 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 lighthsl(210 40% 98%)/ darkhsl(240 10% 5%). - Icons:
lucide-reactonly. NeverDollarSign/BadgeDollarSign→ useBanknote. AI surfaces only branded “Ruby” (N/A here).
Routing / dashboards
- Query-param views (
?view=todos). Rendered whereactiveView === 'todos':ui/src/components/dashboards/DoctorDashboard.tsx(and any other dashboard that renders todos — verify Admin/Receptionist). ui/src/lib/nav-registry.ts—MODULE_PARAMS(params each view owns; cleaned on view switch).ui/src/App.tsx— legacy redirect/doctor/todos → /dashboard?view=todos.
4. Architecture
New componentui/src/components/todos/TodoPlannerPage.tsx replaces MyTodoListPage at every render site. It composes three focused units:
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:
- Overdue —
status==='open'anddueAtcalendar-date < today (red styling, reuseisPastsemantics). - This week — open,
dueAtwithin current ISO week (Mon–Sun) and ≥ today. - This month — open,
dueAtwithin current calendar month but after this week. - Later — open,
dueAtbeyond this month. - No due date — open,
dueAt == null. - Done —
status==='done'(collapsed footer;archivedexcluded).
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 aSchedulerProvider, mirroringAppointmentCalendar.tsx’s adapter. - Render
WeekViewV2in 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 +
getWeekNumberfor the header (Month YYYY / W##).
4.3 All-day todo chips
Surface open todos whosedueAt 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):
- 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). - Fallback (if the component structure makes the prop awkward): render the chip strip as a sibling row in
TodoWeekCalendar, with column widths matched toWeekViewV2’s gutter + 7 equal columns.
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 date→null). See Risk R1 re:bodyrequirement. - Open/edit: click row/chip → existing
TodoFormPagevia 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. ReuseuseDeviceType.
4.6 URL state & nav
- Keep
?view=todos. Reuse/extend URL params:?q=(search),?action=new,?id=(form),?date=(week anchor). UpdateMODULE_PARAMS['todos']innav-registry.tsto owndate(and keepq,action,id) so they aren’t stripped on view switch.?todoTabis retired (buckets replace tabs). - Nav item, label (“Todos”), permissions: unchanged. No edits to
permissions.ts(client or server) orclinic_modules.
5. Data flow
- On mount / week change: fetch todos (
listDoctorTodos, admin→all, doctor→own) and the week’s appointments (getAppointments). - Bucket todos client-side (PKT). Render rail.
- Seed
SchedulerProviderwith appointments; render read-onlyWeekViewV2; overlay all-day chips from due-dated todos. - 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
tagsarray.) - 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 whetherPOST /todosserver validation requiresbody. 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 —
WeekViewV2regression. 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. AvoidtoISOString()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
WeekViewV2in this module does not allow reschedule/create (could mutate real appointments). Gate via areadOnlyprop or wrapper. - R6 — Render-site coverage.
MyTodoListPagemay be rendered by more than one dashboard. Swap allactiveView==='todos'render sites toTodoPlannerPage. RemoveMyTodoListPageonly after parity is confirmed (extract reusable bits likeTodoRowmeta 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=todosand legacy/doctor/todosstill 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.tsxui/src/components/todos/_planner/TodoRail.tsx,TodoBucketSection.tsx,TodoRailRow.tsx,QuickAddRow.tsx,TodoWeekCalendar.tsx,TodoAllDayStrip.tsxui/src/components/todos/lib/bucketTodos.ts(+ unit-testable date logic)
- Dashboard render sites of
MyTodoListPage→TodoPlannerPage(DoctorDashboard.tsx, and Admin/Receptionist if applicable). ui/src/lib/nav-registry.ts—MODULE_PARAMS['todos']adddate.- Possibly
ui/src/components/schedule/_components/view/week/week-view-v2.tsx— optionalrenderDayBannerprop +readOnly(additive only). server/src/routes/doctor-todos.ts— only if R1 requires relaxingbodyvalidation.
ui/src/components/todos/MyTodoListPage.tsx(extract reusable row meta first).
permissions.ts (client+server), clinic_modules, serverComm todo CRUD, TodoFormPage.
