added pending removal ui

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-20 12:56:46 +00:00
parent 254125ddb7
commit c64c7a7bdf
5 changed files with 126 additions and 26 deletions

View file

@ -9,7 +9,7 @@ import {
TabsTrigger,
} from "@/app/shadcn_components/ui/tabs";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { BarChart2, Table2, FolderOpen, Wrench } from "lucide-react";
import { BarChart2, Table2, FolderOpen, Wrench, AlertTriangle } from "lucide-react";
import DrillDownTable from "./DrillDownTable";
import PropertyTable from "./PropertyTable";
import DocumentTable from "./DocumentTable";
@ -24,6 +24,7 @@ import type {
ClassifiedDeal,
DocumentDrawerState,
DocStatusMap,
RemovalStatusByDeal,
} from "./types";
export default function LiveTracker({
@ -33,6 +34,7 @@ export default function LiveTracker({
docStatusMap,
userCapability,
approvalsByDeal,
removalStatusByDeal,
portfolioId,
userRole,
userEmail,
@ -49,6 +51,12 @@ export default function LiveTracker({
(p) => p.projectCode === currentProjectCode,
);
// ── Pending removal count for current project ────────────────────────
const pendingRemovalCount = (currentProject?.allDeals ?? []).filter((d) => {
const state = d.dealId ? removalStatusByDeal[d.dealId] : undefined;
return state === "pending_removal" || state === "pending_re_addition";
}).length;
// ── Drill-down table modal (used by AnalyticsView) ───────────────────
const [openTable, setOpenTable] = useState<TableModal | null>(null);
@ -117,6 +125,14 @@ export default function LiveTracker({
>
<Table2 className="h-3.5 w-3.5" />
Properties
<span
className={`ml-1 min-w-[18px] h-[18px] px-1 rounded-full bg-amber-500 text-white text-[10px] font-bold flex items-center justify-center leading-none transition-opacity ${
pendingRemovalCount > 0 ? "opacity-100" : "opacity-0 pointer-events-none"
}`}
aria-hidden={pendingRemovalCount === 0}
>
{pendingRemovalCount || ""}
</span>
</TabsTrigger>
<TabsTrigger
value="documents"
@ -180,11 +196,19 @@ export default function LiveTracker({
</div>
)}
<div className={`flex items-center gap-2.5 px-4 py-3 rounded-xl border border-amber-200 bg-amber-50 text-amber-800 text-sm ${pendingRemovalCount === 0 ? "hidden" : ""}`}>
<AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />
<span>
<span className="font-semibold">{pendingRemovalCount}</span>{" "}
{pendingRemovalCount === 1 ? "property has" : "properties have"} an outstanding removal request
</span>
</div>
<PropertyTable
data={currentProject?.allDeals ?? []}
onOpenDrawer={handleOpenDrawer}
onOpenDetail={setDetailDeal}
docStatusMap={docStatusMap}
removalStatusByDeal={removalStatusByDeal}
/>
</div>
</TabsContent>

View file

@ -38,7 +38,7 @@ import {
import { Search, SlidersHorizontal, ChevronLeft, ChevronRight, Download } from "lucide-react";
import { createPropertyTableColumns } from "./PropertyTableColumns";
import { STAGE_ORDER } from "./types";
import type { ClassifiedDeal, DocStatusMap } from "./types";
import type { ClassifiedDeal, DocStatusMap, RemovalStatusByDeal, EffectiveRemovalState } from "./types";
// Human-readable labels for toggle dropdown
const COLUMN_LABELS: Record<string, string> = {
@ -58,6 +58,7 @@ const COLUMN_LABELS: Record<string, string> = {
};
type DocFilter = "all" | "has_docs" | "incomplete" | "none";
type RemovalFilter = "all" | "pending_removal" | "removed" | "pending_re_addition";
interface PropertyTableProps {
data: ClassifiedDeal[];
@ -65,6 +66,7 @@ interface PropertyTableProps {
onOpenDetail?: (deal: ClassifiedDeal) => void;
showDocuments?: boolean;
docStatusMap?: DocStatusMap;
removalStatusByDeal?: RemovalStatusByDeal;
}
const CSV_FIELDS: { key: keyof ClassifiedDeal; label: string }[] = [
@ -96,10 +98,11 @@ function escapeCell(value: unknown): string {
: str;
}
export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDocuments = false, docStatusMap = {} }: PropertyTableProps) {
export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDocuments = false, docStatusMap = {}, removalStatusByDeal = {} }: PropertyTableProps) {
const [globalFilter, setGlobalFilter] = useState("");
const [stageFilter, setStageFilter] = useState<string>("all");
const [docFilter, setDocFilter] = useState<DocFilter>("all");
const [removalFilter, setRemovalFilter] = useState<RemovalFilter>("all");
const [sorting, setSorting] = useState<SortingState>([]);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@ -117,7 +120,7 @@ export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDo
fullLodgementDate: false,
});
// Pre-filter by stage and doc status before TanStack gets it
// Pre-filter by stage, doc status, and removal status before TanStack gets it
const filteredData = useMemo(() => {
let result = data;
if (stageFilter !== "all") {
@ -132,12 +135,18 @@ export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDo
return true;
});
}
if (removalFilter !== "all") {
result = result.filter((d) => {
const state: EffectiveRemovalState = (d.dealId ? removalStatusByDeal[d.dealId] : undefined) ?? "none";
return state === removalFilter;
});
}
return result;
}, [data, stageFilter, docFilter, docStatusMap]);
}, [data, stageFilter, docFilter, docStatusMap, removalFilter, removalStatusByDeal]);
const columns = useMemo(
() => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap, onOpenDetail),
[onOpenDrawer, showDocuments, docStatusMap, onOpenDetail]
() => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap, onOpenDetail, removalStatusByDeal),
[onOpenDrawer, showDocuments, docStatusMap, onOpenDetail, removalStatusByDeal]
);
const table = useReactTable({
@ -227,6 +236,31 @@ export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDo
</SelectContent>
</Select>
{/* Removal status filter */}
<Select
value={removalFilter}
onValueChange={(v) => {
setRemovalFilter(v as RemovalFilter);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
<SelectTrigger className="h-9 w-[180px] text-sm border-gray-200 shrink-0">
{removalFilter === "all"
? "All properties"
: removalFilter === "pending_removal"
? "Pending removal"
: removalFilter === "removed"
? "Removed"
: "Pending re-addition"}
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All properties</SelectItem>
<SelectItem value="pending_removal">Pending removal</SelectItem>
<SelectItem value="removed">Removed</SelectItem>
<SelectItem value="pending_re_addition">Pending re-addition</SelectItem>
</SelectContent>
</Select>
{/* Docs filter */}
{showDocuments && (
<Select

View file

@ -3,7 +3,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, CheckCircle2, AlertCircle, FileX } from "lucide-react";
import { STAGE_COLORS } from "./types";
import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types";
import type { ClassifiedDeal, DisplayStage, DocStatusMap, RemovalStatusByDeal } from "./types";
// -----------------------------------------------------------------------
// Stage badge — consistent pill rendering
@ -49,6 +49,7 @@ export function createPropertyTableColumns(
showDocuments: boolean = false,
docStatusMap: DocStatusMap = {},
onOpenDetail?: (deal: ClassifiedDeal) => void,
removalStatusByDeal: RemovalStatusByDeal = {},
): ColumnDef<ClassifiedDeal>[] {
const columns: ColumnDef<ClassifiedDeal>[] = [
// ── Address ──────────────────────────────────────────────────────────
@ -56,22 +57,29 @@ export function createPropertyTableColumns(
accessorKey: "dealname",
id: "dealname",
header: ({ column }) => <SortableHeader label="Address" column={column as any} />,
cell: ({ row }) => (
<div className="max-w-[220px]">
{onOpenDetail ? (
<button
onClick={() => onOpenDetail(row.original)}
className="text-sm font-medium text-brandblue hover:text-brandmidblue hover:underline underline-offset-2 leading-tight text-left truncate w-full transition-colors"
>
{row.original.dealname ?? "—"}
</button>
) : (
<p className="text-sm font-medium text-gray-900 leading-tight truncate">
{row.original.dealname ?? "—"}
</p>
)}
</div>
),
cell: ({ row }) => {
const removalState = row.original.dealId ? removalStatusByDeal[row.original.dealId] : undefined;
const hasPending = removalState === "pending_removal" || removalState === "pending_re_addition";
return (
<div className="max-w-[220px] flex items-center gap-1.5">
{hasPending && (
<span className="shrink-0 w-2 h-2 rounded-full bg-amber-400" title="Outstanding removal request" />
)}
{onOpenDetail ? (
<button
onClick={() => onOpenDetail(row.original)}
className="text-sm font-medium text-brandblue hover:text-brandmidblue hover:underline underline-offset-2 leading-tight text-left truncate transition-colors"
>
{row.original.dealname ?? "—"}
</button>
) : (
<p className="text-sm font-medium text-gray-900 leading-tight truncate">
{row.original.dealname ?? "—"}
</p>
)}
</div>
);
},
enableHiding: false,
},

View file

@ -1,7 +1,7 @@
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { redirect } from "next/navigation";
import { eq, inArray, and } from "drizzle-orm";
import { eq, inArray, and, desc } from "drizzle-orm";
import LiveTracker from "./LiveTracker";
import { computeLiveTrackerData } from "./transforms";
import { db } from "@/app/db/db";
@ -11,8 +11,9 @@ import { portfolioOrganisation } from "@/app/db/schema/portfolio_organisation";
import { organisation } from "@/app/db/schema/organisation";
import { portfolioCapabilities, portfolioUsers } from "@/app/db/schema/portfolio";
import { dealMeasureApprovals } from "@/app/db/schema/approvals";
import { propertyRemovalRequests } from "@/app/db/schema/removal_requests";
import { user as userTable } from "@/app/db/schema/users";
import type { HubspotDeal, DocStatusMap, DocStatus, PortfolioCapabilityType, ApprovalsByDeal } from "./types";
import type { HubspotDeal, DocStatusMap, DocStatus, PortfolioCapabilityType, ApprovalsByDeal, RemovalStatusByDeal, EffectiveRemovalState } from "./types";
import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES, SURVEY_ALL_DOC_TYPES } from "./types";
import type { InferSelectModel } from "drizzle-orm";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
@ -195,6 +196,34 @@ export default async function LiveReportingPage(props: {
}
}
// Compute effective removal state per deal
const removalStatusByDeal: RemovalStatusByDeal = {};
const removalRows = await db
.select({
hubspotDealId: propertyRemovalRequests.hubspotDealId,
type: propertyRemovalRequests.type,
status: propertyRemovalRequests.status,
})
.from(propertyRemovalRequests)
.where(eq(propertyRemovalRequests.portfolioId, BigInt(portfolioId)))
.orderBy(desc(propertyRemovalRequests.requestedAt));
// Keep only the most recent row per deal, then derive effective state
const seenDeals = new Set<string>();
for (const row of removalRows) {
if (seenDeals.has(row.hubspotDealId)) continue;
seenDeals.add(row.hubspotDealId);
let state: EffectiveRemovalState = "none";
if (row.status === "pending") {
state = row.type === "re_addition" ? "pending_re_addition" : "pending_removal";
} else if (row.type === "removal" && row.status === "approved") {
state = "removed";
} else if (row.type === "re_addition" && row.status === "declined") {
state = "removed";
}
if (state !== "none") removalStatusByDeal[row.hubspotDealId] = state;
}
// Fetch survey document status for all properties
const uprnList = deals
.map((d) => d.uprn)
@ -273,6 +302,7 @@ export default async function LiveReportingPage(props: {
docStatusMap={docStatusMap}
userCapability={userCapability}
approvalsByDeal={approvalsByDeal}
removalStatusByDeal={removalStatusByDeal}
portfolioId={portfolioId}
userRole={userRole}
userEmail={userEmail ?? ""}

View file

@ -169,6 +169,9 @@ export type PortfolioCapabilityType = ("approver" | "contractor")[];
// Approved measure names per HubSpot deal ID
export type ApprovalsByDeal = Record<string, string[]>;
export type EffectiveRemovalState = "none" | "pending_removal" | "removed" | "pending_re_addition";
export type RemovalStatusByDeal = Record<string, EffectiveRemovalState>;
// -----------------------------------------------------------------------
// Removal request record returned by the API
// -----------------------------------------------------------------------
@ -194,6 +197,7 @@ export type LiveTrackerProps = {
docStatusMap: DocStatusMap;
userCapability: PortfolioCapabilityType;
approvalsByDeal: ApprovalsByDeal;
removalStatusByDeal: RemovalStatusByDeal;
portfolioId: string;
userRole: string;
userEmail: string;