From 9b0156afc8aab718c4ec930853a8997196bda6ce Mon Sep 17 00:00:00 2001 From: DJP Date: Tue, 21 Apr 2026 16:46:41 -0400 Subject: [PATCH] Project detail: always show all fields + admin-editable + owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes the user flagged on the /projects/:id page: - Metadata panel rendered only populated fields; empty ones vanished entirely. A freshly-created project showed "Business Unit: Display" and nothing else, making admins think half the fields didn't exist. Now all fields always render with an em-dash ("—") for empty — obvious what's missing, and fmt helpers keep dates/money clean. - Default the collapsible "Project Details" to OPEN. Was collapsed, which hid the whole form behind a click. - New Edit button inside the details panel header (admins only). Reuses the existing ProjectFormDialog in edit mode with defaultValues populated from the loaded project; submit goes via useUpdateProject (already existed). Toast on success/error. - "Owner" is now a labeled field in the panel — maps to the existing Project.requestor column (same field the XLSX intake column is named "Owner"; the form dialog already labeled it that way but the read view called it "Requestor"). Also added Description block above the metadata grid so narrative context isn't hidden behind the Edit dialog. --- src/app/(app)/projects/[projectId]/page.tsx | 209 +++++++++++++++----- 1 file changed, 162 insertions(+), 47 deletions(-) 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"), + }); + }} + /> + )} ); }