Add schedule timeline updates and V2 roadmap preview#32
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a “fixed schedule events” capability (DB + UI) and updates project status messaging to mark V1 as stable while previewing V2 direction.
Changes:
- Adds
is_fixed_event,schedule_start_at,schedule_end_atto tasks (with an index for fixed events). - Introduces schedule timeline UI + modal for creating fixed-time blocks, and allows clicking a todo to open task details.
- Updates base styles for new schedule components and refreshes README status/roadmap.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/005_add_task_schedule_columns.sql | Adds schedule-related columns + partial index for fixed events. |
| src/types/index.ts | Extends Task type with schedule-related fields. |
| src/styles/base.css | Adds styling for date/time inputs, schedule timeline, and modal sizing. |
| src/components/TodoListContainer.tsx | Plumbs through onSelectTodoTask callback. |
| src/components/TodoList.tsx | Makes todo items keyboard/click selectable and adjusts layout for project tag. |
| src/components/ScheduleTimeline.tsx | New schedule timeline component that renders free/occupied segments. |
| src/components/App.tsx | Wires schedule section/modal, derives schedule events, and opens task detail modal from todo list. |
| README.md | Updates project status to “V1 stable” and adds V2 preview section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const taskDayKey = rawStart.toISOString().slice(0, 10); | ||
| const dayStartForTask = new Date(`${taskDayKey}T00:00:00`); | ||
| const dayEndForTask = new Date(`${taskDayKey}T23:59:59.999`); |
There was a problem hiding this comment.
taskDayKey 通过 rawStart.toISOString().slice(0, 10) 取的是 UTC 日期;而 todayKey/scheduleDate 是本地日期字符串。两者混用会导致在非 UTC 时区下把事件归到错误的日期列(例如本地凌晨/深夜的事件)。建议用本地时间生成 dateKey(基于 rawStart.getFullYear()/getMonth()/getDate()),并确保后续 dayStartForTask 也基于同一套本地日历日。
| const [scheduleDate, setScheduleDate] = useState(defaultDate ?? new Date().toISOString().slice(0, 10)); | ||
| const [startTime, setStartTime] = useState('08:00'); | ||
| const [endTime, setEndTime] = useState('09:00'); |
There was a problem hiding this comment.
scheduleDate 默认值使用 new Date().toISOString().slice(0, 10) 取的是 UTC 日期,用户本地时区可能会默认到“昨天/明天”。建议改用与全局一致的本地日期生成方式(例如复用 getLocalDateKey()),保证日期输入的默认值符合用户本地日期。
| const dates = useMemo(() => { | ||
| const result: string[] = []; | ||
| for (let i = 0; i < viewDays; i++) { | ||
| const d = new Date(todayKey); |
There was a problem hiding this comment.
这里同样使用 new Date(todayKey) 解析 YYYY-MM-DD,会按 UTC 解析并可能造成日期偏移,进而导致 timeline 的列标题/列数与 todayKey(本地日)不一致。建议用 new Date(${todayKey}T00:00:00) 或拆分年月日构造本地日期。
| const d = new Date(todayKey); | |
| const d = new Date(`${todayKey}T00:00:00`); |
| segments.push({ | ||
| kind: 'occupied', | ||
| startMinute: event.startMinute, | ||
| endMinute: event.endMinute, | ||
| id: event.id, | ||
| name: event.name, | ||
| }); |
There was a problem hiding this comment.
当存在重叠或乱序数据时(例如后端写入或未来功能引入),这里会在 cursor 已经过了某个时间段的情况下仍然 push 一个从 event.startMinute 开始的 occupied 段,导致总时长被重复计算、渲染高度超过 24h。建议在生成 occupied 段时将开始时间裁剪为 max(cursor, event.startMinute),或在 normalized 阶段合并/裁剪重叠区间。
| segments.push({ | |
| kind: 'occupied', | |
| startMinute: event.startMinute, | |
| endMinute: event.endMinute, | |
| id: event.id, | |
| name: event.name, | |
| }); | |
| const occupiedStart = Math.max(cursor, event.startMinute); | |
| if (occupiedStart < event.endMinute) { | |
| segments.push({ | |
| kind: 'occupied', | |
| startMinute: occupiedStart, | |
| endMinute: event.endMinute, | |
| id: event.id, | |
| name: event.name, | |
| }); | |
| } |
| ADD COLUMN IF NOT EXISTS is_fixed_event boolean NOT NULL DEFAULT false, | ||
| ADD COLUMN IF NOT EXISTS schedule_start_at timestamp with time zone, | ||
| ADD COLUMN IF NOT EXISTS schedule_end_at timestamp with time zone; | ||
|
|
There was a problem hiding this comment.
当前 schema 允许 is_fixed_event = true 但 schedule_start_at/schedule_end_at 为空或结束早于开始(前端校验可绕过/未来接口可能写入)。建议在 DB 层增加约束(例如 CHECK:(NOT is_fixed_event) OR (schedule_start_at IS NOT NULL AND schedule_end_at IS NOT NULL AND schedule_end_at > schedule_start_at)),以保证数据一致性并减少后续代码中的防御性判断。
| -- Enforce valid fixed-event schedule ranges at the database layer | |
| ALTER TABLE public.tasks | |
| ADD CONSTRAINT tasks_fixed_event_schedule_check | |
| CHECK ( | |
| (NOT is_fixed_event) | |
| OR ( | |
| schedule_start_at IS NOT NULL | |
| AND schedule_end_at IS NOT NULL | |
| AND schedule_end_at > schedule_start_at | |
| ) | |
| ); |
| is_fixed_event?: boolean; | ||
| schedule_start_at?: string | null; | ||
| schedule_end_at?: string | null; |
There was a problem hiding this comment.
Task 接口顶部注释写的是“严格对齐 Supabase schema”,但这里把 is_fixed_event(DB 中 NOT NULL + DEFAULT false)以及新时间字段都声明成可选属性,会让类型系统允许缺字段的 task 并掩盖数据一致性问题。建议把 is_fixed_event 设为必填 boolean,并将 schedule_start_at/schedule_end_at 设为必填但可空(string | null),与 schema 对齐。
| is_fixed_event?: boolean; | |
| schedule_start_at?: string | null; | |
| schedule_end_at?: string | null; | |
| is_fixed_event: boolean; | |
| schedule_start_at: string | null; | |
| schedule_end_at: string | null; |
| const d = new Date(todayKey); | ||
| d.setDate(d.getDate() + i); | ||
| const y = d.getFullYear(); | ||
| const m = String(d.getMonth() + 1).padStart(2, '0'); | ||
| const dd = String(d.getDate()).padStart(2, '0'); | ||
| dates.push(`${y}-${m}-${dd}`); |
There was a problem hiding this comment.
这里用 new Date(todayKey) 解析 YYYY-MM-DD 的日期字符串在 JS 中会按 UTC 解析(且不同时区可能出现日期偏移),随后再用 setDate 叠加天数,容易导致生成的 dates 列表和本地 todayKey 不一致。建议用 new Date(${todayKey}T00:00:00)(本地 0 点)或手动拆分 year/month/day 构造,避免时区偏移。
Summary
This PR finalizes the current schedule phase and prepares the repository for a stable V1 period.
Included changes
Why
The goal is to close this phase cleanly, merge to
main, and keep a stable version available for at least the next month while V2 planning proceeds in isolation.Notes
npm run build.