From 7860ca6ad1685a26252c0512a62a7a484152ad31 Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Sun, 22 Feb 2026 15:15:30 +0100 Subject: [PATCH] feat: add inline project timeline and notes section to Project Detail - Implemented a Gantt timeline using GanttChart component scoped to project checkpoints. - Added functionality to create and manage checkpoints with AddCheckpointDialog. - Introduced EditCheckpointDialog for editing existing checkpoints. - Created a notes section displaying a list of notes with the ability to add new notes. - Updated routing to include notes detail page. - Enhanced GanttChart with context menu for editing and deleting checkpoints. - Improved UI components for better user experience. --- DEFAULT_PROMPT.md | 20 +- prd.json | 4 +- progress.txt | 20 ++ .../components/projects/ProjectDetail.tsx | 143 +++++++++- .../timeline/AddCheckpointDialog.tsx | 42 ++- .../timeline/EditCheckpointDialog.tsx | 107 +++++++ .../components/timeline/GanttChart.tsx | 266 +++++++++++------- src/renderer/components/ui/hover-card.tsx | 42 +++ src/renderer/routeTree.gen.ts | 24 +- src/renderer/routes/notes.$noteId.tsx | 34 +++ src/renderer/routes/timeline.tsx | 22 +- 11 files changed, 595 insertions(+), 129 deletions(-) create mode 100644 src/renderer/components/timeline/EditCheckpointDialog.tsx create mode 100644 src/renderer/components/ui/hover-card.tsx create mode 100644 src/renderer/routes/notes.$noteId.tsx diff --git a/DEFAULT_PROMPT.md b/DEFAULT_PROMPT.md index c360431..b688434 100644 --- a/DEFAULT_PROMPT.md +++ b/DEFAULT_PROMPT.md @@ -23,20 +23,20 @@ APPEND to progress.txt (never replace, always append): ## USER REQUEST { - "id": "US-014", - "title": "Kanban board in Project Detail", - "description": "As a user, I want a Kanban board inside the project detail view with drag-and-drop task management between status columns.", + "id": "US-015", + "title": "Inline project timeline and notes list in Project Detail", + "description": "As a user, I want to see the project's Gantt timeline and a list of its notes within the project detail scrollable view.", "acceptanceCriteria": [ - "@hello-pangea/dnd installed; DragDropContext wraps 3 Droppable columns: To Do | In Progress | Completed", - "Each task card is a Draggable wrapped in a shadcn/ui Card rendering: title, description (truncated), priority as shadcn/ui Badge, due date chip, assignee string", - "Dragging a card to another column calls tasks.update({ id, status }) via tRPC and the UI updates immediately (optimistic or on success)", - "'+ Add' shadcn/ui Button (variant=ghost, size=sm) in each column header opens the shadcn/ui Dialog new-task modal with the column's status pre-selected", - "Columns show a task count in their header using shadcn/ui Badge (variant=secondary)", - "All card content uses shadcn/ui primitives: Card, Badge, Button (already installed)", + "Project Detail view includes a 'Project Timeline' section using the GanttChart component (from US-012) scoped to the current project's checkpoints", + "'+ Add' shadcn/ui Button (variant=outline, size=sm) in the timeline section header opens the add-checkpoint shadcn/ui Dialog with the project pre-selected", + "Notes section below Kanban shows a flat list using shadcn/ui Separator between rows: each row has note title + formatted createdAt date", + "'+ Add' shadcn/ui Button in notes header calls notes.create with a default title and navigates to /notes/:noteId", + "Clicking a note title navigates to /notes/:noteId", + "All buttons/dialogs use shadcn/ui components (already installed)", "Typecheck passes", "Verify in browser using dev-browser skill" ], - "priority": 14, + "priority": 15, "passes": false, "notes": "" } \ No newline at end of file diff --git a/prd.json b/prd.json index f58ce49..4f9a383 100644 --- a/prd.json +++ b/prd.json @@ -278,8 +278,8 @@ "Verify in browser using dev-browser skill" ], "priority": 15, - "passes": false, - "notes": "" + "passes": true, + "notes": "Completed: Project Timeline section using GanttChart (scoped to project checkpoints) with AddCheckpointDialog (defaultProjectId hides project selector), checkpoint delete mutation. Notes section using Item cards (variant=muted) in flex-wrap grid matching Figma design, '+ Add' creates note via notes.create and navigates to /notes/$noteId, clicking note card navigates to /notes/$noteId. Route stub at notes.$noteId.tsx with back button + note title." }, { "id": "US-016", diff --git a/progress.txt b/progress.txt index e79be37..f493636 100644 --- a/progress.txt +++ b/progress.txt @@ -249,3 +249,23 @@ - When grouping tasks by status for Kanban columns, always handle unknown/null status values with a fallback to prevent tasks from disappearing - `DragDropContext.onDragEnd` provides `draggableId` which maps directly to `task.id` — no need to look up the task object for status updates --- + +## 2026-02-22 - US-015 +- What was implemented: + - Added Project Timeline section to `ProjectDetail.tsx` between AI Summary and Tasks Kanban + - Reused `GanttChart` component (from US-012) scoped to current project's checkpoints + - "+ Add" Button (variant=outline, size=sm) opens `AddCheckpointDialog` with `defaultProjectId={projectId}` (hides project selector) + - Wired `checkpoints.delete` mutation with `onDelete` prop for checkpoint dot deletion + - Computed `ganttStart`/`ganttEnd` dynamically from checkpoint dates with 1-month padding (fallback ±2 months if empty) + - Added Notes section below Tasks Kanban using `Item` component (variant=muted) in a flex-wrap grid layout matching Figma design + - Each note card shows `SquareDashed` icon + title + formatted createdAt date, clickable to navigate to `/notes/$noteId` + - "+ Add" Button calls `notes.create({ title: 'Untitled Note', content: '', projectId })` then navigates to the new note + - Created route stub at `src/renderer/routes/notes.$noteId.tsx` with back button + note title placeholder (full editor deferred to US-016) +- Files changed: `src/renderer/components/projects/ProjectDetail.tsx`, `src/renderer/routes/notes.$noteId.tsx` (new), `prd.json`, `progress.txt` +- **Learnings for future iterations:** + - GanttChart + AddCheckpointDialog are designed for reuse: `defaultProjectId` prop scopes the dialog to a project and hides the project select dropdown + - Figma notes section uses a card grid layout (flex-wrap with Item cards), not a flat list with Separators — always cross-reference Figma when acceptance criteria text diverges + - `trpc.useUtils()` provides `invalidate()` for cache busting after mutations — use at the component level, not inside mutation callbacks + - `notes.create` returns `{ id }` which can be used directly for navigation in the `onSuccess` callback + - TanStack Router file-based routing: `notes.$noteId.tsx` generates `/notes/:noteId` route automatically — `Route.useParams()` provides typed `{ noteId }` +--- diff --git a/src/renderer/components/projects/ProjectDetail.tsx b/src/renderer/components/projects/ProjectDetail.tsx index c042c3f..d12d324 100644 --- a/src/renderer/components/projects/ProjectDetail.tsx +++ b/src/renderer/components/projects/ProjectDetail.tsx @@ -1,5 +1,7 @@ -import { useMemo, useState } from 'react'; -import { Sparkles, FileText, CheckCircle2, Milestone, Plus } from 'lucide-react'; +import { Fragment, useMemo, useState } from 'react'; +import { Sparkles, FileText, CheckCircle2, Milestone, Plus, SquareDashed } from 'lucide-react'; +import { format } from 'date-fns'; +import { useNavigate } from '@tanstack/react-router'; import { trpc } from '@/lib/trpc'; import { Button } from '@/components/ui/button'; import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription } from '@/components/ui/item'; @@ -10,6 +12,9 @@ import { BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { KanbanBoard } from './KanbanBoard'; +import { GanttChart, type GanttCheckpoint } from '@/components/timeline/GanttChart'; +import { AddCheckpointDialog } from '@/components/timeline/AddCheckpointDialog'; +import { EditCheckpointDialog } from '@/components/timeline/EditCheckpointDialog'; type ProjectDetailProps = { projectId: string; @@ -17,6 +22,10 @@ type ProjectDetailProps = { export function ProjectDetail({ projectId }: ProjectDetailProps) { const [newTaskOpen, setNewTaskOpen] = useState(false); + const [addCheckpointOpen, setAddCheckpointOpen] = useState(false); + const [editingCheckpoint, setEditingCheckpoint] = useState(null); + const navigate = useNavigate(); + const utils = trpc.useUtils(); const { data: project, isLoading } = trpc.projects.get.useQuery({ id: projectId }); const { data: clientsList } = trpc.clients.list.useQuery(); const { data: notesList } = trpc.notes.list.useQuery({ projectId }); @@ -54,6 +63,52 @@ export function ProjectDetail({ projectId }: ProjectDetailProps) { return { approved, total: all.length }; }, [checkpointsList]); + // Map checkpoints to GanttChart format + const ganttCheckpoints: GanttCheckpoint[] = useMemo(() => { + return (checkpointsList ?? []).map((c) => ({ + id: c.id, + title: c.title, + date: c.date, + projectId, + isAiSuggested: c.isAiSuggested, + isApproved: c.isApproved, + })); + }, [checkpointsList, projectId]); + + const { ganttStart, ganttEnd } = useMemo(() => { + const now = new Date(); + if (ganttCheckpoints.length === 0) { + const start = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const end = new Date(now.getFullYear(), now.getMonth() + 3, 0); + return { ganttStart: start, ganttEnd: end }; + } + const dates = ganttCheckpoints.map((c) => c.date); + const minDate = new Date(Math.min(...dates)); + const maxDate = new Date(Math.max(...dates)); + const start = new Date(minDate.getFullYear(), minDate.getMonth() - 1, 1); + const end = new Date(maxDate.getFullYear(), maxDate.getMonth() + 2, 0); + return { ganttStart: start, ganttEnd: end }; + }, [ganttCheckpoints]); + + const deleteCheckpoint = trpc.checkpoints.delete.useMutation({ + onSuccess: () => { + void utils.checkpoints.list.invalidate({ projectId }); + }, + }); + + const updateCheckpoint = trpc.checkpoints.update.useMutation({ + onSuccess: () => { + void utils.checkpoints.list.invalidate({ projectId }); + }, + }); + + const createNote = trpc.notes.create.useMutation({ + onSuccess: (data) => { + void utils.notes.list.invalidate({ projectId }); + void navigate({ to: '/notes/$noteId', params: { noteId: data.id } }); + }, + }); + if (isLoading) { return (
@@ -78,10 +133,12 @@ export function ProjectDetail({ projectId }: ProjectDetailProps) { {breadcrumbPath.map((segment, i) => ( - + {i > 0 && } - {segment} - + + {segment} + + ))} @@ -135,11 +192,41 @@ export function ProjectDetail({ projectId }: ProjectDetailProps) { + {/* Project Timeline */} +
+
+

Project Timeline

+ +
+ deleteCheckpoint.mutate({ id })} + onEdit={(cp) => setEditingCheckpoint(cp)} + onToggleApproval={(id, current) => + updateCheckpoint.mutate({ id, isApproved: current === 1 ? 0 : 1 }) + } + /> + + { if (!open) setEditingCheckpoint(null); }} + /> +
+ {/* Tasks Kanban */}

Tasks

- @@ -150,6 +237,50 @@ export function ProjectDetail({ projectId }: ProjectDetailProps) { onNewTaskOpenChange={setNewTaskOpen} />
+ + {/* Notes */} +
+
+

Notes

+ +
+ {notesList && notesList.length > 0 ? ( +
+ {notesList.map((note) => ( + + void navigate({ to: '/notes/$noteId', params: { noteId: note.id } }) + } + > + + + + + {note.title} + + {format(new Date(note.createdAt), 'PPP')} + + + + ))} +
+ ) : ( +

No notes yet.

+ )} +
); } diff --git a/src/renderer/components/timeline/AddCheckpointDialog.tsx b/src/renderer/components/timeline/AddCheckpointDialog.tsx index 7570a8b..87e65e1 100644 --- a/src/renderer/components/timeline/AddCheckpointDialog.tsx +++ b/src/renderer/components/timeline/AddCheckpointDialog.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { format } from 'date-fns'; -import { Calendar as CalendarIcon } from 'lucide-react'; +import { Calendar as CalendarIcon, Check } from 'lucide-react'; import { trpc } from '@/lib/trpc'; import { Button } from '@/components/ui/button'; import { @@ -28,10 +28,16 @@ interface AddCheckpointDialogProps { defaultProjectId?: string; } +interface AddedEntry { + title: string; + date: Date; +} + export function AddCheckpointDialog({ open, onOpenChange, defaultProjectId }: AddCheckpointDialogProps) { const [title, setTitle] = useState(''); const [date, setDate] = useState(); const [projectId, setProjectId] = useState(defaultProjectId ?? ''); + const [added, setAdded] = useState([]); const showProjectSelect = !defaultProjectId; const { data: projectsList } = trpc.projects.listAll.useQuery(undefined, { @@ -40,16 +46,19 @@ export function AddCheckpointDialog({ open, onOpenChange, defaultProjectId }: Ad const utils = trpc.useUtils(); const createCheckpoint = trpc.checkpoints.create.useMutation({ - onSuccess: () => { + onSuccess: (_data, variables) => { void utils.checkpoints.list.invalidate(); - resetAndClose(); + setAdded((prev) => [...prev, { title: variables.title, date: new Date(variables.date) }]); + setTitle(''); + setDate(undefined); }, }); - function resetAndClose() { + function handleClose() { setTitle(''); setDate(undefined); setProjectId(defaultProjectId ?? ''); + setAdded([]); onOpenChange(false); } @@ -68,11 +77,25 @@ export function AddCheckpointDialog({ open, onOpenChange, defaultProjectId }: Ad const canSubmit = title.trim() && date && (defaultProjectId || projectId); return ( - + { if (!v) handleClose(); else onOpenChange(v); }}> - Add Checkpoint + Add Checkpoints + + {/* Just-added list */} + {added.length > 0 && ( +
+ {added.map((entry, i) => ( +
+ + {entry.title} + {format(entry.date, 'MMM d')} +
+ ))} +
+ )} +
@@ -121,11 +145,11 @@ export function AddCheckpointDialog({ open, onOpenChange, defaultProjectId }: Ad )} -
diff --git a/src/renderer/components/timeline/EditCheckpointDialog.tsx b/src/renderer/components/timeline/EditCheckpointDialog.tsx new file mode 100644 index 0000000..2d0ffa1 --- /dev/null +++ b/src/renderer/components/timeline/EditCheckpointDialog.tsx @@ -0,0 +1,107 @@ +import { useState, useEffect } from 'react'; +import { format } from 'date-fns'; +import { Calendar as CalendarIcon } from 'lucide-react'; +import { trpc } from '@/lib/trpc'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Calendar } from '@/components/ui/calendar'; +import { cn } from '@/lib/utils'; +import type { GanttCheckpoint } from './GanttChart'; + +interface EditCheckpointDialogProps { + checkpoint: GanttCheckpoint | null; + onOpenChange: (open: boolean) => void; +} + +export function EditCheckpointDialog({ checkpoint, onOpenChange }: EditCheckpointDialogProps) { + const [title, setTitle] = useState(''); + const [date, setDate] = useState(); + + const utils = trpc.useUtils(); + + useEffect(() => { + if (checkpoint) { + setTitle(checkpoint.title); + setDate(new Date(checkpoint.date)); + } + }, [checkpoint]); + + const updateCheckpoint = trpc.checkpoints.update.useMutation({ + onSuccess: () => { + void utils.checkpoints.list.invalidate(); + onOpenChange(false); + }, + }); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!checkpoint || !title.trim() || !date) return; + + updateCheckpoint.mutate({ + id: checkpoint.id, + title: title.trim(), + date: date.getTime(), + }); + } + + const canSubmit = title.trim() && date; + + return ( + + + + Edit Checkpoint + +
+ setTitle(e.target.value)} + required + autoFocus + /> + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/src/renderer/components/timeline/GanttChart.tsx b/src/renderer/components/timeline/GanttChart.tsx index 443a07c..2f15ef2 100644 --- a/src/renderer/components/timeline/GanttChart.tsx +++ b/src/renderer/components/timeline/GanttChart.tsx @@ -1,7 +1,14 @@ -import { useRef, useState, useEffect, useCallback } from 'react'; +import { useRef, useEffect, useState, useCallback } from 'react'; import { format } from 'date-fns'; +import { Pencil, Trash2 } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { Button } from '@/components/ui/button'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from '@/components/ui/context-menu'; export interface GanttCheckpoint { id: string; @@ -18,12 +25,15 @@ interface GanttChartProps { startDate: Date; endDate: Date; onDelete?: (id: string) => void; + onEdit?: (checkpoint: GanttCheckpoint) => void; + onToggleApproval?: (id: string, currentApproved: number) => void; } const HEADER_HEIGHT = 30; const BASELINE_Y = 70; -const SVG_HEIGHT = 100; -const DOT_RADIUS = 7; +const BASELINE_HEIGHT = 8; +const SVG_HEIGHT = 110; +const DOT_RADIUS = 10; const PADDING_X = 40; function getMonthsBetween(start: Date, end: Date): Date[] { @@ -43,7 +53,14 @@ function dateToX(date: Date, start: Date, end: Date, width: number): number { return PADDING_X + ratio * (width - PADDING_X * 2); } -export function GanttChart({ checkpoints, startDate, endDate, onDelete }: GanttChartProps) { +export function GanttChart({ + checkpoints, + startDate, + endDate, + onDelete, + onEdit, + onToggleApproval, +}: GanttChartProps) { const containerRef = useRef(null); const [width, setWidth] = useState(600); @@ -63,10 +80,11 @@ export function GanttChart({ checkpoints, startDate, endDate, onDelete }: GanttC const months = getMonthsBetween(startDate, endDate); const todayX = dateToX(new Date(), startDate, endDate, width); + const todayVisible = todayX >= PADDING_X && todayX <= width - PADDING_X; return (
- + {/* Month labels */} {months.map((month) => { const x = dateToX(month, startDate, endDate, width); @@ -86,8 +104,8 @@ export function GanttChart({ checkpoints, startDate, endDate, onDelete }: GanttC x1={x} y1={HEADER_HEIGHT} x2={x} - y2={BASELINE_Y + 10} - stroke="#e5e5e5" + y2={BASELINE_Y + 14} + stroke="var(--border)" strokeWidth={1} strokeDasharray="4 4" /> @@ -95,41 +113,47 @@ export function GanttChart({ checkpoints, startDate, endDate, onDelete }: GanttC ); })} - {/* Baseline */} - - {/* Today marker */} - {todayX >= PADDING_X && todayX <= width - PADDING_X && ( - - - - Today - - + {/* Today marker — dashed line */} + {todayVisible && ( + )} - {/* Checkpoint dots rendered as foreignObject for Popover */} + {/* Today marker — Badge label via foreignObject */} + {todayVisible && ( + +
+ + Today + +
+
+ )} + + {/* Checkpoint dots */} {checkpoints.map((cp) => { const cx = dateToX(new Date(cp.date), startDate, endDate, width); return ( @@ -138,6 +162,8 @@ export function GanttChart({ checkpoints, startDate, endDate, onDelete }: GanttC checkpoint={cp} cx={cx} onDelete={onDelete} + onEdit={onEdit} + onToggleApproval={onToggleApproval} /> ); })} @@ -146,83 +172,131 @@ export function GanttChart({ checkpoints, startDate, endDate, onDelete }: GanttC ); } +function getStatusLabel(checkpoint: GanttCheckpoint): { text: string; className: string } { + if (checkpoint.isApproved === 0) { + return { text: 'Pending', className: 'bg-muted text-muted-foreground' }; + } + if (checkpoint.date < Date.now()) { + return { text: 'Completed', className: 'bg-chart-2/15 text-chart-2' }; + } + return { text: 'To Do', className: 'bg-primary/10 text-primary' }; +} + function CheckpointDot({ checkpoint, cx, onDelete, + onEdit, + onToggleApproval, }: { checkpoint: GanttCheckpoint; cx: number; onDelete?: (id: string) => void; + onEdit?: (checkpoint: GanttCheckpoint) => void; + onToggleApproval?: (id: string, currentApproved: number) => void; }) { - const [open, setOpen] = useState(false); + const [hovered, setHovered] = useState(false); + const hoverTimeout = useRef | null>(null); - const handleDelete = useCallback(() => { - onDelete?.(checkpoint.id); - setOpen(false); - }, [onDelete, checkpoint.id]); + const handleMouseEnter = useCallback(() => { + if (hoverTimeout.current) clearTimeout(hoverTimeout.current); + hoverTimeout.current = setTimeout(() => setHovered(true), 200); + }, []); + + const handleMouseLeave = useCallback(() => { + if (hoverTimeout.current) clearTimeout(hoverTimeout.current); + hoverTimeout.current = setTimeout(() => setHovered(false), 150); + }, []); - // Determine dot style - // isApproved=0 (pending AI suggestion) → dashed outline - // isApproved=1 + date in past → green (#16a34a) = completed - // isApproved=1 + date in future → dark (#171717) = todo const isPending = checkpoint.isApproved === 0; const isPast = checkpoint.date < Date.now(); - const fill = isPending ? 'none' : (isPast ? '#16a34a' : '#171717'); - const stroke = isPending ? '#737373' : 'none'; + const fill = isPending ? 'none' : (isPast ? 'var(--chart-2)' : 'var(--primary)'); + const stroke = isPending ? 'var(--muted-foreground)' : 'none'; const strokeDasharray = isPending ? '3 2' : undefined; + const status = getStatusLabel(checkpoint); + + const dotSize = DOT_RADIUS * 2 + 2; + const hitArea = dotSize + 8; + + const dotSvg = ( + + + + ); return ( - - - - - -
-
{checkpoint.title}
-
- {format(new Date(checkpoint.date), 'PPP')} -
- {checkpoint.projectName && ( -
- Project: {checkpoint.projectName} -
- )} - {onDelete && ( - - )} -
-
-
+ {dotSvg} + + + + + e.preventDefault()} + > +
+
+ + {checkpoint.title} + + + {status.text} + +
+ + {format(new Date(checkpoint.date), 'PPP')} + + {checkpoint.projectName && ( + + {checkpoint.projectName} + + )} +
+
+ + + + onEdit?.(checkpoint)}> + + Edit Checkpoint + + onDelete?.(checkpoint.id)} + className="text-destructive focus:text-destructive" + > + + Delete Checkpoint + + +
); } diff --git a/src/renderer/components/ui/hover-card.tsx b/src/renderer/components/ui/hover-card.tsx new file mode 100644 index 0000000..ab58b3f --- /dev/null +++ b/src/renderer/components/ui/hover-card.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import { HoverCard as HoverCardPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function HoverCard({ + ...props +}: React.ComponentProps) { + return +} + +function HoverCardTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function HoverCardContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/renderer/routeTree.gen.ts b/src/renderer/routeTree.gen.ts index aa39bdf..15bb327 100644 --- a/src/renderer/routeTree.gen.ts +++ b/src/renderer/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as TimelineRouteImport } from './routes/timeline' import { Route as TasksRouteImport } from './routes/tasks' import { Route as ProjectsRouteImport } from './routes/projects' import { Route as IndexRouteImport } from './routes/index' +import { Route as NotesNoteIdRouteImport } from './routes/notes.$noteId' const TimelineRoute = TimelineRouteImport.update({ id: '/timeline', @@ -34,18 +35,25 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const NotesNoteIdRoute = NotesNoteIdRouteImport.update({ + id: '/notes/$noteId', + path: '/notes/$noteId', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/projects': typeof ProjectsRoute '/tasks': typeof TasksRoute '/timeline': typeof TimelineRoute + '/notes/$noteId': typeof NotesNoteIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/projects': typeof ProjectsRoute '/tasks': typeof TasksRoute '/timeline': typeof TimelineRoute + '/notes/$noteId': typeof NotesNoteIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -53,13 +61,14 @@ export interface FileRoutesById { '/projects': typeof ProjectsRoute '/tasks': typeof TasksRoute '/timeline': typeof TimelineRoute + '/notes/$noteId': typeof NotesNoteIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/projects' | '/tasks' | '/timeline' + fullPaths: '/' | '/projects' | '/tasks' | '/timeline' | '/notes/$noteId' fileRoutesByTo: FileRoutesByTo - to: '/' | '/projects' | '/tasks' | '/timeline' - id: '__root__' | '/' | '/projects' | '/tasks' | '/timeline' + to: '/' | '/projects' | '/tasks' | '/timeline' | '/notes/$noteId' + id: '__root__' | '/' | '/projects' | '/tasks' | '/timeline' | '/notes/$noteId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -67,6 +76,7 @@ export interface RootRouteChildren { ProjectsRoute: typeof ProjectsRoute TasksRoute: typeof TasksRoute TimelineRoute: typeof TimelineRoute + NotesNoteIdRoute: typeof NotesNoteIdRoute } declare module '@tanstack/react-router' { @@ -99,6 +109,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/notes/$noteId': { + id: '/notes/$noteId' + path: '/notes/$noteId' + fullPath: '/notes/$noteId' + preLoaderRoute: typeof NotesNoteIdRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -107,6 +124,7 @@ const rootRouteChildren: RootRouteChildren = { ProjectsRoute: ProjectsRoute, TasksRoute: TasksRoute, TimelineRoute: TimelineRoute, + NotesNoteIdRoute: NotesNoteIdRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/renderer/routes/notes.$noteId.tsx b/src/renderer/routes/notes.$noteId.tsx new file mode 100644 index 0000000..a581b91 --- /dev/null +++ b/src/renderer/routes/notes.$noteId.tsx @@ -0,0 +1,34 @@ +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { ArrowLeft } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { trpc } from '@/lib/trpc'; + +export const Route = createFileRoute('/notes/$noteId')({ + component: NoteDetailPage, +}); + +function NoteDetailPage() { + const { noteId } = Route.useParams(); + const navigate = useNavigate(); + const { data: note } = trpc.notes.get.useQuery({ id: noteId }); + + return ( +
+
+ +

+ {note?.title ?? 'Loading...'} +

+
+

+ Note editor will be implemented in US-016 (Milkdown). +

+
+ ); +} diff --git a/src/renderer/routes/timeline.tsx b/src/renderer/routes/timeline.tsx index a348632..ea2e4b7 100644 --- a/src/renderer/routes/timeline.tsx +++ b/src/renderer/routes/timeline.tsx @@ -5,6 +5,7 @@ import { trpc } from '@/lib/trpc'; import { Button } from '@/components/ui/button'; import { GanttChart, type GanttCheckpoint } from '@/components/timeline/GanttChart'; import { AddCheckpointDialog } from '@/components/timeline/AddCheckpointDialog'; +import { EditCheckpointDialog } from '@/components/timeline/EditCheckpointDialog'; export const Route = createFileRoute('/timeline')({ component: TimelinePage, @@ -12,6 +13,7 @@ export const Route = createFileRoute('/timeline')({ function TimelinePage() { const [dialogOpen, setDialogOpen] = useState(false); + const [editingCheckpoint, setEditingCheckpoint] = useState(null); const { data: checkpoints } = trpc.checkpoints.list.useQuery({}); const { data: projectsList } = trpc.projects.listAll.useQuery(); @@ -23,6 +25,12 @@ function TimelinePage() { }, }); + const updateCheckpoint = trpc.checkpoints.update.useMutation({ + onSuccess: () => { + void utils.checkpoints.list.invalidate(); + }, + }); + // Build project name lookup const projectMap = useMemo(() => { const map = new Map(); @@ -75,15 +83,15 @@ function TimelinePage() { {/* Legend */}
- + To Do
- + Completed
- + AI Suggestion (Pending)
@@ -100,11 +108,19 @@ function TimelinePage() { startDate={startDate} endDate={endDate} onDelete={(id) => deleteCheckpoint.mutate({ id })} + onEdit={(cp) => setEditingCheckpoint(cp)} + onToggleApproval={(id, current) => + updateCheckpoint.mutate({ id, isApproved: current === 1 ? 0 : 1 }) + } />
)} + { if (!open) setEditingCheckpoint(null); }} + />
); }