From c12e64754602dfaa305c3825d965abb2ca05fee5 Mon Sep 17 00:00:00 2001 From: DJP Date: Tue, 21 Apr 2026 14:49:07 -0400 Subject: [PATCH] Gate XLSX + per-stage notes + expanded deliverable info panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. /projects Import XLSX now behind the same isAdmin gate as New Project. Producers won't see either — bulk + manual intake are both admin-only escape hatches. 2. Per-stage notes. Backend was already wired (DeliverableStage.notes + updateStageSchema.notes + PATCH handler); just no UI. - New StageNotes component — inline editor per stage row. Empty state shows a "+ Add note" link; existing notes render as muted italic text, click anywhere on them to edit. Save on blur or ⌘/Ctrl+Enter, Esc to cancel. - useUpdateStageNotes hook for the PATCH. - Wired into each stage row on the deliverable detail page. 3. Deliverable info panel expanded + promoted above Pipeline Stages. - getDeliverable service now includes parent project context (id, name, projectCode, omgJobNumber, clientTeam) so the UI doesn't need a second round-trip. - Metadata grid always renders; empty fields show em-dash so producers can spot what's missing. Project row at the top surfaces project name, code, OMG #, client team. Deliverable notes are now shown as a block alongside the rest. --- .../deliverables/[deliverableId]/page.tsx | 170 +++++++++++++----- src/app/(app)/projects/page.tsx | 24 +-- src/components/stages/stage-notes.tsx | 137 ++++++++++++++ src/hooks/use-deliverables.ts | 21 +++ src/lib/services/deliverable-service.ts | 13 ++ 5 files changed, 308 insertions(+), 57 deletions(-) create mode 100644 src/components/stages/stage-notes.tsx diff --git a/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx b/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx index 4357a5a..25fd556 100644 --- a/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx +++ b/src/app/(app)/projects/[projectId]/deliverables/[deliverableId]/page.tsx @@ -24,11 +24,33 @@ import { useUpdateStageStatus, } from "@/hooks/use-deliverables"; import { StageDatePopover } from "@/components/stages/stage-date-popover"; +import { StageNotes } from "@/components/stages/stage-notes"; import { PipelineProgress } from "@/components/deliverables/pipeline-progress"; import { StageStatusBadge } from "@/components/stages/stage-status-badge"; import { TooltipProvider } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; +// Compact label+value cell for the deliverable info grid. Value accepts +// a ReactNode so links/badges can be passed in. +function MetaField({ + label, + value, + mono, +}: { + label: string; + value: React.ReactNode; + mono?: boolean; +}) { + return ( +
+

+ {label} +

+

{value}

+
+ ); +} + const PRIORITY_STYLES: Record = { LOW: "bg-[var(--status-not-started)]/10 text-[var(--status-not-started)]", MEDIUM: "bg-[var(--status-in-progress)]/10 text-[var(--status-in-progress)]", @@ -150,64 +172,114 @@ export default function DeliverableDetailPage() { - {/* Deliverable metadata fields */} - {(deliverable.cmfSku || - deliverable.assetCount != null || - deliverable.requestedDueDate || - deliverable.plannedDeliveryDate || - deliverable.actualDeliveryDate || - deliverable.wfInputDate) && ( -
+ {/* Deliverable metadata — always rendered, empty fields show an + em-dash so producers see the whole picture + know which fields + are missing data. Project context sits at the top so you never + lose track of which project this deliverable belongs to. */} +
+

+ Deliverable Info +

+
+ {deliverable.project && ( + <> + + {deliverable.project.name} + + } + /> + + + + + )} + + + + + + + + {deliverable.cmfSku && (

CMF / SKU

-

{deliverable.cmfSku}

+

{deliverable.cmfSku}

)} - {deliverable.assetCount != null && ( -
+ {deliverable.notes && ( +

- Asset Count + Deliverable Notes

-

{deliverable.assetCount}

-
- )} - {deliverable.requestedDueDate && ( -
-

- Requested Due Date -

-

{format(new Date(deliverable.requestedDueDate), "MMM d, yyyy")}

-
- )} - {deliverable.plannedDeliveryDate && ( -
-

- Planned Delivery -

-

{format(new Date(deliverable.plannedDeliveryDate), "MMM d, yyyy")}

-
- )} - {deliverable.actualDeliveryDate && ( -
-

- Actual Delivery -

-

{format(new Date(deliverable.actualDeliveryDate), "MMM d, yyyy")}

-
- )} - {deliverable.wfInputDate && ( -
-

- WF Input Date -

-

{format(new Date(deliverable.wfInputDate), "MMM d, yyyy")}

+

{deliverable.notes}

)}
- )} +
{/* Pipeline overview */} @@ -360,6 +432,12 @@ export default function DeliverableDetailPage() { Waiting for prerequisite stages
)} + + {/* Free-text notes per stage — producers + assignees can + attach context (e.g. "waiting on spec", "ref attached", + "needs legal review"). Always editable; empty state + is a compact "+ Add note" affordance. */} +
); })} diff --git a/src/app/(app)/projects/page.tsx b/src/app/(app)/projects/page.tsx index dcf6b46..716d73e 100644 --- a/src/app/(app)/projects/page.tsx +++ b/src/app/(app)/projects/page.tsx @@ -134,9 +134,9 @@ export default function ProjectsPage() { color: s.color ?? null, })); }, [pipelineTemplates]); - // Manual "New Project" is ADMIN-only — normal intake flow is via the - // OMG webhook. Producers can still edit + delete existing projects via - // the row-level controls. (XLSX bulk import left open for now.) + // Manual "New Project" and "Import XLSX" are ADMIN-only — normal + // intake flow is via the OMG webhook. Producers can still edit + + // delete existing projects via the row-level controls. const isAdmin = useIsAdmin(); const createProject = useCreateProject(); const deleteProject = useDeleteProject(); @@ -287,15 +287,17 @@ export default function ProjectsPage() { Export CSV - {isAdmin && ( - + <> + + + )} diff --git a/src/components/stages/stage-notes.tsx b/src/components/stages/stage-notes.tsx new file mode 100644 index 0000000..14343c4 --- /dev/null +++ b/src/components/stages/stage-notes.tsx @@ -0,0 +1,137 @@ +"use client"; + +/** + * Inline per-stage note editor for the deliverable detail page. + * + * Design: + * - When no note and not editing → subtle "+ Add note" link. + * - When a note exists → show it with a pencil-click hit area; click + * anywhere on the text enters edit mode. + * - Editing → textarea; Save on Enter or blur, Cancel on Escape. + * + * Backend already has DeliverableStage.notes; uses useUpdateStageNotes + * hook which invalidates the deliverable query so the row re-renders + * with the fresh value. + */ + +import { useEffect, useRef, useState } from "react"; +import { Pencil, Plus } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useUpdateStageNotes } from "@/hooks/use-deliverables"; +import { toast } from "sonner"; + +interface StageNotesProps { + stageId: string; + notes: string | null; + /** Hide when the viewer shouldn't be able to edit. Non-destructive: + * existing notes still render, they just can't be changed. */ + readOnly?: boolean; +} + +export function StageNotes({ stageId, notes, readOnly = false }: StageNotesProps) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(notes ?? ""); + const textareaRef = useRef(null); + const update = useUpdateStageNotes(); + + // Sync draft when the note prop changes from outside (e.g., another + // browser tab or a refetch). Avoid clobbering in-progress edits. + useEffect(() => { + if (!editing) setDraft(notes ?? ""); + }, [notes, editing]); + + useEffect(() => { + if (editing && textareaRef.current) { + textareaRef.current.focus(); + // Move cursor to the end of existing text + const len = textareaRef.current.value.length; + textareaRef.current.setSelectionRange(len, len); + } + }, [editing]); + + const commit = () => { + const trimmed = draft.trim(); + const next = trimmed.length === 0 ? null : trimmed; + if (next === (notes ?? null)) { + setEditing(false); + return; + } + update.mutate( + { stageId, notes: next }, + { + onSuccess: () => { + setEditing(false); + }, + onError: (e: any) => { + toast.error(e.message ?? "Failed to save note"); + }, + } + ); + }; + + const cancel = () => { + setDraft(notes ?? ""); + setEditing(false); + }; + + if (editing) { + return ( +
+