import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { Sparkles, FileText, CheckCircle2, Milestone, Plus } 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'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { Card, CardContent } from '@/components/ui/card'; import { KanbanBoard } from './KanbanBoard'; import { GanttChart, type GanttCheckpoint } from '@/components/timeline/GanttChart'; import { AddCheckpointDialog } from '@/components/timeline/AddCheckpointDialog'; import { EditCheckpointDialog } from '@/components/timeline/EditCheckpointDialog'; import { useFloatingChat } from '@/context/FloatingChatContext'; type ProjectDetailProps = { projectId: string; }; export function ProjectDetail({ projectId }: ProjectDetailProps) { const [newTaskOpen, setNewTaskOpen] = useState(false); const [addCheckpointOpen, setAddCheckpointOpen] = useState(false); const [editingCheckpoint, setEditingCheckpoint] = useState(null); const navigate = useNavigate(); // AI section refs const summaryRef = useRef(null); const timelineRef = useRef(null); const tasksRef = useRef(null); const notesRef = useRef(null); const { registerSection, unregisterSection } = useFloatingChat(); useEffect(() => { registerSection({ id: 'project-summary', label: 'Project Summary', ref: summaryRef, projectId }); registerSection({ id: 'project-timeline', label: 'Project Timeline', ref: timelineRef, projectId }); registerSection({ id: 'project-tasks', label: 'Tasks', ref: tasksRef, projectId }); registerSection({ id: 'project-notes', label: 'Notes', ref: notesRef, projectId }); return () => { unregisterSection('project-summary'); unregisterSection('project-timeline'); unregisterSection('project-tasks'); unregisterSection('project-notes'); }; }, [projectId, registerSection, unregisterSection]); 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 }); const { data: tasksList } = trpc.tasks.list.useQuery({ projectId }); const { data: checkpointsList } = trpc.checkpoints.list.useQuery({ projectId }); // Build breadcrumb path: Client > Sub-Client const breadcrumbPath = useMemo(() => { if (!project?.clientId || !clientsList) return []; const clientMap = new Map(clientsList.map((c) => [c.id, c])); const client = clientMap.get(project.clientId); if (!client) return []; // If client has a parent, show parent > client if (client.parentId) { const parent = clientMap.get(client.parentId); if (parent) return [parent.name, client.name]; } return [client.name]; }, [project?.clientId, clientsList]); // Compute stats const notesCount = notesList?.length ?? 0; const taskStats = useMemo(() => { const all = tasksList ?? []; const done = all.filter((t) => t.status === 'done').length; return { done, total: all.length }; }, [tasksList]); const checkpointStats = useMemo(() => { const all = checkpointsList ?? []; const approved = all.filter((c) => c.isApproved === 1).length; return { approved, total: all.length }; }, [checkpointsList]); const pendingCheckpoints = useMemo(() => (checkpointsList ?? []).filter((c) => c.isAiSuggested === 1 && c.isApproved === 0), [checkpointsList], ); const pendingTasks = useMemo(() => (tasksList ?? []).filter((t) => t.isAiSuggested === 1 && t.isApproved === 0), [tasksList], ); // 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 updateTask = trpc.tasks.update.useMutation({ onSuccess: () => { void utils.tasks.list.invalidate({ projectId }); }, }); const deleteTask = trpc.tasks.delete.useMutation({ onSuccess: () => { void utils.tasks.list.invalidate({ projectId }); }, }); const suggestCheckpoints = trpc.ai.chat.useMutation({ onSuccess: () => { void utils.checkpoints.list.invalidate({ projectId }); }, }); const suggestTasks = trpc.ai.chat.useMutation({ onSuccess: () => { void utils.tasks.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 (
Loading project...
); } if (!project) { return (
Project not found
); } return (
{/* Breadcrumb + Project Name */}
{breadcrumbPath.length > 0 && ( {breadcrumbPath.map((segment, i) => ( {i > 0 && } {segment} ))} )}

{project.name}

{/* Project Summary Section */}
{/* Stat Cards */}
{notesCount} Notes {taskStats.done}/{taskStats.total} Tasks Complete {checkpointStats.approved}/{checkpointStats.total} Checkpoints
{/* AI Project Summary */} AI Project Summary {project.aiSummary || 'AI summary will appear here'}
{/* Project Timeline */}

Project Timeline

deleteCheckpoint.mutate({ id })} onEdit={(cp) => setEditingCheckpoint(cp)} onToggleApproval={(id, current) => updateCheckpoint.mutate({ id, isApproved: current === 1 ? 0 : 1 }) } /> {pendingCheckpoints.length > 0 && (
{pendingCheckpoints.map((cp) => (
{cp.title} {format(new Date(cp.date), 'PPP')}
))}
)} { if (!open) setEditingCheckpoint(null); }} />
{/* Tasks Kanban */}

Tasks

{pendingTasks.length > 0 && (
{pendingTasks.map((t) => (
{t.title} {t.description && ( {t.description} )}
))}
)}
{/* 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.

)}
); }