diff --git a/src/app/(app)/projects/[projectId]/page.tsx b/src/app/(app)/projects/[projectId]/page.tsx index 9bbd8cc..dcf70b4 100644 --- a/src/app/(app)/projects/[projectId]/page.tsx +++ b/src/app/(app)/projects/[projectId]/page.tsx @@ -13,6 +13,7 @@ import { Upload, ChevronRight, Users, + Pencil, } from "lucide-react"; import { format } from "date-fns"; import { Button } from "@/components/ui/button"; @@ -37,7 +38,7 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; -import { useProject } from "@/hooks/use-projects"; +import { useProject, useUpdateProject } from "@/hooks/use-projects"; import { useDeliverables, useCreateDeliverable, @@ -46,9 +47,12 @@ import { import { PipelineProgress } from "@/components/deliverables/pipeline-progress"; import { StageStatusBadge } from "@/components/stages/stage-status-badge"; import { DeliverableFormDialog } from "@/components/deliverables/deliverable-form-dialog"; +import { ProjectFormDialog } from "@/components/projects/project-form-dialog"; import { ExportButton } from "@/components/excel/export-button"; import { ImportDialog } from "@/components/excel/import-dialog"; import { useIsAdmin } from "@/hooks/use-current-user"; +import { toast } from "sonner"; +import { format as formatDate } from "date-fns"; const PRIORITY_STYLES: Record = { LOW: "bg-[var(--status-not-started)]/10 text-[var(--status-not-started)]", @@ -58,12 +62,20 @@ const PRIORITY_STYLES: Record = { }; function MetaField({ label, value }: { label: string; value: string }) { + const isEmpty = value === "—"; return (

{label}

-

{value}

+

+ {value} +

); } @@ -72,61 +84,104 @@ function ProjectMetadataPanel({ project, open, onOpenChange, + canEdit, + onEdit, }: { project: any; open: boolean; onOpenChange: (open: boolean) => void; + canEdit: boolean; + onEdit: () => void; }) { - const fields: [string, string | undefined][] = [ - ["Business Unit", project.businessUnit], - ["Form Factor", project.formFactor], - ["Code Name", project.codeName], - ["NPI / Refresh", project.npiOrRefresh], - ["Quarter", project.quarter], - ["Requestor", project.requestor], - ["Agency", project.agency], - ["Workfront ID", project.workfrontId], - ["OMG Code", project.omgCode], - ["BMT ID", project.bmtId], - [ - "Estimated Cost", - project.estimatedCost != null - ? `$${Number(project.estimatedCost).toFixed(2)}` - : undefined, - ], - [ - "Actual Cost", - project.actualCost != null - ? `$${Number(project.actualCost).toFixed(2)}` - : undefined, - ], + // Always render every field — empty values get an em-dash so admins + // can see at a glance what's missing and hit Edit to fill them. + const EM = "—"; + const fmt = (v: unknown): string => { + if (v == null || v === "") return EM; + return String(v); + }; + const fmtDate = (v: unknown): string => { + if (!v) return EM; + try { + return formatDate(new Date(v as string), "MMM d, yyyy"); + } catch { + return EM; + } + }; + const fmtMoney = (v: unknown): string => { + if (v == null) return EM; + const n = Number(v); + if (!Number.isFinite(n)) return EM; + return `$${n.toFixed(2)}`; + }; + + const fields: Array<[string, string]> = [ + ["OMG Job #", fmt(project.omgJobNumber)], + ["Client Team", fmt(project.clientTeam?.name)], + ["Status", fmt((project.status ?? "").replace?.(/_/g, " "))], + ["Priority", fmt(project.priority)], + ["Owner", fmt(project.requestor)], + ["Category", fmt(project.businessUnit)], + ["Start Date", fmtDate(project.startDate)], + ["Due Date", fmtDate(project.dueDate)], + ["Quarter", fmt(project.quarter)], + ["Form Factor", fmt(project.formFactor)], + ["Code Name", fmt(project.codeName)], + ["NPI / Refresh", fmt(project.npiOrRefresh)], + ["Agency", fmt(project.agency)], + ["Workfront ID", fmt(project.workfrontId)], + ["OMG Code", fmt(project.omgCode)], + ["BMT ID", fmt(project.bmtId)], + ["Estimated Cost", fmtMoney(project.estimatedCost)], + ["Actual Cost", fmtMoney(project.actualCost)], ]; - const populated = fields.filter(([, v]) => v != null && v !== ""); - if (populated.length === 0) return null; return ( - - - +
+ + + + {canEdit && ( + + )} +
+ {project.description && ( +
+

+ Description +

+

+ {project.description} +

+
+ )}
- {populated.map(([label, value]) => ( - + {fields.map(([label, value]) => ( + ))}
@@ -141,15 +196,22 @@ export default function ProjectDetailPage() { const projectId = params.projectId; const [showCreate, setShowCreate] = useState(false); const [showImport, setShowImport] = useState(false); - const [detailsOpen, setDetailsOpen] = useState(false); + const [showEdit, setShowEdit] = useState(false); + // Default open so admins see everything about the project without + // an extra click. Previously collapsed, which made "there's no + // owner field!" a common complaint even when the data was there. + const [detailsOpen, setDetailsOpen] = useState(true); const { data: project, isLoading: projectLoading } = useProject(projectId); - const { data: deliverables, isLoading: delivsLoading } = + const { data: deliverablesRaw, isLoading: delivsLoading } = useDeliverables(projectId); + const deliverables = (deliverablesRaw as any[] | undefined) ?? undefined; const createDeliverable = useCreateDeliverable(projectId); const deleteDeliverable = useDeleteDeliverable(projectId); + const updateProject = useUpdateProject(projectId); // Manual "Add Deliverable" is ADMIN-only — deliverables normally - // arrive via the deliverables webhook. + // arrive via the deliverables webhook. Same gate for editing the + // project itself. const isAdmin = useIsAdmin(); const isLoading = projectLoading || delivsLoading; @@ -219,6 +281,8 @@ export default function ProjectDetailPage() { project={project as any} open={detailsOpen} onOpenChange={setDetailsOpen} + canEdit={isAdmin} + onEdit={() => setShowEdit(true)} /> ) : null} @@ -344,6 +408,57 @@ export default function ProjectDetailPage() { open={showImport} onOpenChange={setShowImport} /> + + {/* Edit project — same ProjectFormDialog used for create, pre- + filled from the loaded project. Only renders when opened so + form defaults reset each edit session. */} + {!!project && showEdit && ( + { + updateProject.mutate(data, { + onSuccess: () => { + toast.success("Project updated"); + setShowEdit(false); + }, + onError: (e: any) => + toast.error(e?.message ?? "Failed to update project"), + }); + }} + /> + )} ); }