From ffe79a27e4b51197f14b2559aefe0492aaad2f9d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Apr 2026 19:24:05 +0000 Subject: [PATCH] adding removal requests --- .../your-projects/live/LiveTracker.tsx | 5 + .../live/PropertyDetailDrawer.tsx | 278 +++++++++++++++++- .../(portfolio)/your-projects/live/page.tsx | 28 +- .../(portfolio)/your-projects/live/types.ts | 16 + 4 files changed, 320 insertions(+), 7 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index 2bb2a4f..0725568 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -34,6 +34,8 @@ export default function LiveTracker({ userCapability, approvalsByDeal, portfolioId, + userRole, + userEmail, }: LiveTrackerProps) { // ── Tab state ──────────────────────────────────────────────────────── const [activeTab, setActiveTab] = useState<"analytics" | "properties" | "documents" | "measures">( @@ -360,6 +362,9 @@ export default function LiveTracker({ deal={detailDeal} portfolioId={portfolioId} onClose={() => setDetailDeal(null)} + userRole={userRole} + userCapability={userCapability} + userEmail={userEmail} /> ); 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 0ca08ce..71175b6 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx @@ -1,19 +1,263 @@ "use client"; import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { motion, AnimatePresence } from "framer-motion"; -import { X, CheckCircle2, Circle, AlertTriangle, ChevronRight, ChevronDown } from "lucide-react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { X, CheckCircle2, Circle, AlertTriangle, ChevronRight, ChevronDown, Trash2 } from "lucide-react"; import { Drawer, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, - DrawerDescription, } 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 } from "./types"; +import type { ClassifiedDeal, PortfolioCapabilityType, RemovalRequest } from "./types"; + +// ----------------------------------------------------------------------- +// Removal request section +// ----------------------------------------------------------------------- +const WRITE_ROLES = ["creator", "admin", "write"]; + +function RemovalRequestSection({ + dealId, + portfolioId, + userRole, + userCapability, +}: { + dealId: string; + portfolioId: string; + userRole: string; + userCapability: PortfolioCapabilityType; +}) { + const queryClient = useQueryClient(); + const [dialogOpen, setDialogOpen] = useState(false); + 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 pendingRequest = data?.requests?.find((r) => r.status === "pending") ?? null; + const latestResolvedRequest = data?.requests?.find((r) => r.status !== "pending") ?? null; + + async function handleSubmit() { + if (!reason.trim()) 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() }), + }); + if (!res.ok) { + const json = await res.json().catch(() => ({})); + setError(json.error ?? "Failed to submit request"); + return; + } + setDialogOpen(false); + setReason(""); + 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); + } + } + + if (isLoading) { + return

Loading…

; + } + + return ( +
+ {error && ( +

{error}

+ )} + + {/* Pending request — visible to everyone */} + {pendingRequest && ( +
+
+ + Pending Removal Request + +
+

{pendingRequest.reason}

+

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

+ {/* Approver actions */} + {isApprover && ( +
+ + +
+ )} +
+ )} + + {/* Most recent resolved request */} + {!pendingRequest && latestResolvedRequest && ( +
+
+ + {latestResolvedRequest.status === "approved" ? "Removal Approved" : "Removal Declined"} + +
+

{latestResolvedRequest.reason}

+

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

+ {latestResolvedRequest.reviewedByEmail && ( +

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

+ )} +
+ )} + + {/* Request button — only shown when no pending request exists */} + {!pendingRequest && ( + + + + + + + + {!canRequest && ( + + Not available with read-only permissions + + )} + + + )} + + {/* Reason dialog */} + { if (!v) { setDialogOpen(false); setReason(""); setError(null); } }}> + + + + Request Removal from Project + + +
+

+ Please provide a reason why this property should be removed from the project. This will be recorded for audit purposes. +

+