From 166cea397b977f4f064097957784bf55e9c00589 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 12 May 2026 11:24:08 +0000 Subject: [PATCH] refactoring details drawer to orchestrator format --- .../live/PropertyDetailDrawer.tsx | 1651 +---------------- .../live/PropertyTableColumns.tsx | 19 +- .../your-projects/live/[dealId]/DealPage.tsx | 54 +- .../deal-detail/MeasureApprovalEditor.tsx | 243 +++ .../deal-detail/PibiSurveysTabContent.tsx | 59 + .../deal-detail/RemovalRequestSection.tsx | 306 +++ .../live/deal-detail/SurveyRequestSection.tsx | 158 ++ .../your-projects/live/deal-detail/index.ts | 16 + .../pibi/InstructMeasureEditor.tsx | 226 +++ .../live/deal-detail/pibi/PibiDatesEditor.tsx | 200 ++ .../deal-detail/pibi/PibiMeasureSelector.tsx | 216 +++ .../live/deal-detail/primitives.tsx | 140 ++ .../(portfolio)/your-projects/live/ui.tsx | 16 + 13 files changed, 1618 insertions(+), 1686 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/MeasureApprovalEditor.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/PibiSurveysTabContent.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/RemovalRequestSection.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/SurveyRequestSection.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/index.ts create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/pibi/InstructMeasureEditor.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/pibi/PibiDatesEditor.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/pibi/PibiMeasureSelector.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/deal-detail/primitives.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/ui.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx index 4f9f1c4..9295690 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx @@ -1,9 +1,7 @@ "use client"; -import { ActivityLog } from "./ActivityLog"; -import { useEffect, useMemo, useState } from "react"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { X, CheckCircle2, Circle, AlertTriangle, ChevronRight, ChevronDown, Trash2, RotateCcw, Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { X, ChevronRight, ChevronDown, AlertTriangle } from "lucide-react"; import { Drawer, DrawerClose, @@ -11,42 +9,25 @@ import { DrawerHeader, DrawerTitle, } from "@/app/shadcn_components/ui/drawer"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/app/shadcn_components/ui/dialog"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/app/shadcn_components/ui/tooltip"; -import { STAGE_COLORS } from "./types"; -import type { ClassifiedDeal, PortfolioCapabilityType, RemovalRequest } from "./types"; import { parseMeasures } from "@/app/lib/parseMeasures"; -import { ApprovalConfirmDialog } from "./ApprovalConfirmDialog"; -import type { PendingDiff } from "./ApprovalConfirmDialog"; -import { MEASURE_NAMES } from "@/app/lib/measureDocumentRequirements"; import { outOfOrderInstructionWarning } from "@/app/lib/softWarnings"; -import { PibiSection } from "./PibiSection"; -import { useToast } from "@/app/hooks/use-toast"; +import type { ClassifiedDeal, PortfolioCapabilityType } from "./types"; +import { StageBadge } from "./ui"; +import { + type DrawerSection, + SECTION_TITLES, + InfoRow, + SectionHeader, + MilestoneTimeline, + formatDate, +} from "./deal-detail/primitives"; +import { MeasureApprovalEditor } from "./deal-detail/MeasureApprovalEditor"; +import { InstructMeasureEditor } from "./deal-detail/pibi/InstructMeasureEditor"; +import { PibiSurveysTabContent } from "./deal-detail/PibiSurveysTabContent"; +import { ActivityLog } from "./ActivityLog"; -// Sections the caller can request focus on. Used by entry-points like the -// Measures table row click that should land the user on a specific tab. -export type DrawerSection = - | "survey" - | "measures" - | "pibi" - | "domna" - | "technical"; - -// The tabs inside the drawer. type DrawerTab = "overview" | "works" | "pibi-surveys"; -// Maps each focusable section to the tab that contains it. const SECTION_TO_TAB: Record = { survey: "overview", measures: "works", @@ -61,1524 +42,6 @@ const TAB_LABELS: Record = { "pibi-surveys": "PIBIs & Surveys", }; -// ----------------------------------------------------------------------- -// Removal request section -// ----------------------------------------------------------------------- -export const WRITE_ROLES = ["creator", "admin", "write"]; - -export function RemovalRequestSection({ - dealId, - portfolioId, - userRole, - userCapability, -}: { - dealId: string; - portfolioId: string; - userRole: string; - userCapability: PortfolioCapabilityType; -}) { - const queryClient = useQueryClient(); - const [dialogType, setDialogType] = useState<"removal" | "re_addition" | null>(null); - const [reason, setReason] = useState(""); - const [submitting, setSubmitting] = useState(false); - const [reviewing, setReviewing] = useState(false); - const [error, setError] = useState(null); - - const canRequest = WRITE_ROLES.includes(userRole); - const isApprover = userCapability.includes("approver"); - - const { data, isLoading } = useQuery<{ requests: RemovalRequest[] }>({ - queryKey: ["removalRequests", portfolioId, dealId], - queryFn: async () => { - const res = await fetch( - `/api/portfolio/${portfolioId}/removal-requests?dealId=${dealId}`, - ); - if (!res.ok) throw new Error("Failed to fetch removal requests"); - return res.json(); - }, - staleTime: 30_000, - }); - - const latest = data?.requests?.[0] ?? null; - - // Derive effective state from the most recent request - type EffectiveState = "active" | "pending_removal" | "removed" | "pending_re_addition"; - const effectiveState: EffectiveState = (() => { - if (!latest) return "active"; - if (latest.status === "pending") { - return latest.type === "re_addition" ? "pending_re_addition" : "pending_removal"; - } - if (latest.type === "removal" && latest.status === "approved") return "removed"; - if (latest.type === "re_addition" && latest.status === "declined") return "removed"; - return "active"; - })(); - - const pendingRequest = latest?.status === "pending" ? latest : null; - const latestResolvedRequest = latest?.status !== "pending" ? latest : null; - - function closeDialog() { - setDialogType(null); - setReason(""); - setError(null); - } - - async function handleSubmit() { - if (!reason.trim() || !dialogType) return; - setSubmitting(true); - setError(null); - try { - const res = await fetch(`/api/portfolio/${portfolioId}/removal-requests`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ hubspotDealId: dealId, reason: reason.trim(), type: dialogType }), - }); - if (!res.ok) { - const json = await res.json().catch(() => ({})); - setError(json.error ?? "Failed to submit request"); - return; - } - closeDialog(); - queryClient.invalidateQueries({ queryKey: ["removalRequests", portfolioId, dealId] }); - } finally { - setSubmitting(false); - } - } - - async function handleReview(requestId: string, action: "approved" | "declined") { - setReviewing(true); - setError(null); - try { - const res = await fetch(`/api/portfolio/${portfolioId}/removal-requests`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ requestId: Number(requestId), action }), - }); - if (!res.ok) { - const json = await res.json().catch(() => ({})); - setError(json.error ?? "Failed to review request"); - return; - } - queryClient.invalidateQueries({ queryKey: ["removalRequests", portfolioId, dealId] }); - } finally { - setReviewing(false); - } - } - - function resolvedLabel(req: RemovalRequest): string { - if (req.type === "re_addition") { - return req.status === "approved" ? "Re-addition Approved" : "Re-addition Declined"; - } - return req.status === "approved" ? "Removal Approved" : "Removal Declined"; - } - - if (isLoading) { - return

Loading…

; - } - - return ( -
- {error && ( -

{error}

- )} - - {/* Pending request — visible to everyone */} - {pendingRequest && ( -
-
- - {pendingRequest.type === "re_addition" ? "Pending Re-addition Request" : "Pending Removal Request"} - -
-

{pendingRequest.reason}

-

- Requested by {pendingRequest.requestedByEmail} - {" · "} - {formatDateTime(pendingRequest.requestedAt)} -

- {isApprover && ( -
- - -
- )} -
- )} - - {/* Most recent resolved request */} - {latestResolvedRequest && ( -
-
- - {resolvedLabel(latestResolvedRequest)} - -
-

{latestResolvedRequest.reason}

-

- Requested by {latestResolvedRequest.requestedByEmail} - {" · "} - {formatDateTime(latestResolvedRequest.requestedAt)} -

- {latestResolvedRequest.reviewedByEmail && ( -

- {latestResolvedRequest.status === "approved" ? "Approved" : "Declined"} by{" "} - {latestResolvedRequest.reviewedByEmail} - {latestResolvedRequest.reviewedAt && ` · ${formatDateTime(latestResolvedRequest.reviewedAt)}`} -

- )} -
- )} - - {/* Action buttons — shown when no pending request */} - {!pendingRequest && ( - <> - {effectiveState === "active" && ( - - - - - - - - {!canRequest && ( - - Not available with read-only permissions - - )} - - - )} - - {effectiveState === "removed" && ( - - - - - - - - {!canRequest && ( - - Not available with read-only permissions - - )} - - - )} - - )} - - {/* Shared dialog for removal and re-addition requests */} - { if (!v) closeDialog(); }}> - - - - {dialogType === "re_addition" ? "Request Re-addition to Project" : "Request Removal from Project"} - - -
-

- {dialogType === "re_addition" - ? "Please provide a reason why this property should be re-added to the project. This will be recorded for audit purposes." - : "Please provide a reason why this property should be removed from the project. This will be recorded for audit purposes."} -

-