"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { FolderOpen, ChevronDown } from "lucide-react"; import { listProjects, updateProject, deleteProject, } from "@/app/lib/mikeApi"; import { OwnerOnlyPopup } from "@/app/components/popups/OwnerOnlyPopup"; import { useAuth } from "@/contexts/AuthContext"; import type { Project } from "@/app/components/shared/types"; import { NewProjectModal } from "./NewProjectModal"; import { ProjectDetailsModal } from "./ProjectDetailsModal"; import { TableToolbar } from "@/app/components/shared/TableToolbar"; import { RowActionMenuItems, RowActions, } from "@/app/components/shared/RowActions"; import { PageHeader } from "@/app/components/shared/PageHeader"; import { TABLE_CHECKBOX_CLASS, TABLE_STICKY_CELL_BG, SkeletonDot, SkeletonLine, TableBody, TableCell, TableEmptyState, TableHeaderCell, TableHeaderRow, TablePrimaryCell, TableRow, TableScrollArea, TableStickyCell, } from "@/app/components/shared/TablePrimitive"; function formatDate(iso: string) { return new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric", }); } function getProjectOwnerLabel(project: Project, currentUserId?: string | null) { if (project.is_owner ?? project.user_id === currentUserId) return "Me"; return ( project.owner_display_name?.trim() || project.owner_email?.trim() || "Shared" ); } type ProjectFilter = "all" | "mine" | "shared-with-me"; export function ProjectsOverview() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [modalOpen, setModalOpen] = useState(false); const [detailsProject, setDetailsProject] = useState(null); const [activeFilter, setActiveFilter] = useState("all"); const [selectedIds, setSelectedIds] = useState([]); const [actionsOpen, setActionsOpen] = useState(false); const [search, setSearch] = useState(""); const [ownerOnlyAction, setOwnerOnlyAction] = useState(null); const actionsRef = useRef(null); const router = useRouter(); const { user, isAuthenticated, authLoading } = useAuth(); useEffect(() => { let cancelled = false; async function loadProjects() { await Promise.resolve(); if (cancelled) return; if (authLoading) { setLoading(true); return; } if (!isAuthenticated) { setProjects([]); setLoadError(null); setLoading(false); return; } setLoading(true); setLoadError(null); try { const loaded = await listProjects(); if (!cancelled) setProjects(loaded); } catch (err) { console.error("[projects] failed to load projects", err); if (!cancelled) { setProjects([]); setLoadError("Could not load projects."); } } finally { if (!cancelled) setLoading(false); } } void loadProjects(); return () => { cancelled = true; }; }, [authLoading, isAuthenticated, user?.id]); useEffect(() => { function handleClick(e: MouseEvent) { if ( actionsRef.current && !actionsRef.current.contains(e.target as Node) ) setActionsOpen(false); } if (actionsOpen) document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [actionsOpen]); const q = search.toLowerCase(); const filtered = ( activeFilter === "all" ? projects : activeFilter === "mine" ? projects.filter((p) => p.is_owner ?? p.user_id === user?.id) : projects.filter((p) => !(p.is_owner ?? p.user_id === user?.id)) ).filter( (p) => !q || p.name.toLowerCase().includes(q) || (p.cm_number ?? "").toLowerCase().includes(q) || (p.practice ?? "").toLowerCase().includes(q), ); const allSelected = filtered.length > 0 && filtered.every((p) => selectedIds.includes(p.id)); const someSelected = !allSelected && filtered.some((p) => selectedIds.includes(p.id)); function toggleAll() { if (allSelected) { setSelectedIds([]); } else { setSelectedIds(filtered.map((p) => p.id)); } } function toggleOne(id: string) { setSelectedIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], ); } const filters: { id: ProjectFilter; label: string }[] = [ { id: "all", label: "All" }, { id: "mine", label: "Mine" }, { id: "shared-with-me", label: "Shared with me" }, ]; async function handleProjectDetailsSave(values: { name: string; cmNumber: string; practice: string; }) { if (!detailsProject) return; if ( detailsProject.is_owner === false || (user?.id && detailsProject.user_id !== user.id) ) { setOwnerOnlyAction("edit project details"); return; } const name = values.name.trim(); const cmNumber = values.cmNumber.trim(); const practice = values.practice.trim(); if (!name) return; const updated = await updateProject(detailsProject.id, { name, cm_number: cmNumber, practice: practice || null, }); setProjects((prev) => prev.map((project) => project.id === updated.id ? { ...project, ...updated } : project, ), ); setDetailsProject((current) => current?.id === updated.id ? { ...current, ...updated } : current, ); } async function handleDeleteSelected() { const ids = [...selectedIds]; setActionsOpen(false); // Only the project owner can delete; the per-row delete is hidden // for shared projects but the bulk action can still pick them up // if a user toggled them across filters. Filter and warn. const owned = ids.filter((id) => { const p = projects.find((pp) => pp.id === id); return !p || (p.is_owner ?? p.user_id === user?.id); }); const blocked = ids.length - owned.length; setSelectedIds([]); await Promise.all(owned.map((id) => deleteProject(id).catch(() => {}))); setProjects((prev) => prev.filter((p) => !owned.includes(p.id))); if (blocked > 0) { setOwnerOnlyAction( `delete ${blocked} of the selected projects — only the project owner can delete a project`, ); } } const toolbarActions = ( <> {selectedIds.length > 0 && (
{actionsOpen && (
)}
)} ); return (
{/* Page header */} setModalOpen(true), title: "New project", }, ]} >

Projects

{ setActiveFilter(nextFilter); setSelectedIds([]); }} actions={toolbarActions} /> {/* Table */} {loading ? ( ) : ( { if (el) el.indeterminate = someSelected; }} onChange={toggleAll} className={TABLE_CHECKBOX_CLASS} /> )} Name CM Practice Owner Files Chats Tabular Reviews Created } > {loading ? ( {[1, 2, 3].map((i) => ( ))} ) : loadError ? (

Projects

{loadError}

) : filtered.length === 0 ? ( {activeFilter === "all" || activeFilter === "mine" ? ( <>

Projects

Upload documents into projects and to commence chats and tabular reviews with them.

) : (

No {activeFilter} projects

)}
) : ( {filtered.map((project) => { const rowBg = selectedIds.includes(project.id) ? "bg-gray-50" : TABLE_STICKY_CELL_BG; return ( ( { setDetailsProject(project); }} onDelete={async () => { await deleteProject( project.id, ); setProjects((prev) => prev.filter( (p) => p.id !== project.id, ), ); }} /> ) : undefined } onClick={() => { router.push(`/projects/${project.id}`); }} > {/* Project Name */} toggleOne(project.id) } label={project.name} /> {project.cm_number ?? ( )} {project.practice ?? ( )} {getProjectOwnerLabel(project, user?.id)} {project.document_count ?? 0} {project.chat_count ?? 0} {project.review_count ?? 0} {formatDate(project.created_at)}
e.stopPropagation()} > {(project.is_owner ?? project.user_id === user?.id) && ( { setDetailsProject(project); }} onDelete={async () => { await deleteProject(project.id); setProjects((prev) => prev.filter( (p) => p.id !== project.id, ), ); }} /> )}
); })}
)}
setModalOpen(false)} onCreated={(p) => { setProjects((prev) => [p, ...prev]); router.push(`/projects/${p.id}`); }} /> setDetailsProject(null)} onSave={handleProjectDetailsSave} /> setOwnerOnlyAction(null)} />
); }