From 6605223865324e6c13851ce9c9488e2a2d7a72c5 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 31 Mar 2026 14:17:46 +0000 Subject: [PATCH] working on reporting page --- .../your-projects/live/LiveTracker.tsx | 338 +++------ .../your-projects/live/ProgressOverview.tsx | 706 ++++++++++-------- .../(portfolio)/your-projects/live/page.tsx | 52 +- .../your-projects/live/transforms.ts | 167 ++++- .../(portfolio)/your-projects/live/types.ts | 172 ++++- 5 files changed, 829 insertions(+), 606 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 2765db04..ccdb2d56 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -1,41 +1,51 @@ "use client"; import { useState } from "react"; -import ProgressOverview from "./ProgressOverview"; -import SurveyedResultsPieChart from "./SurveyedResultsPieChart"; -import TableViewer from "./TableViewer"; -import { Card, CardContent } from "@/app/shadcn_components/ui/card"; -import { Home, AlertTriangle } from "lucide-react"; import { motion } from "framer-motion"; -import type { LiveTrackerProps, TableModal, ClassifiedDeal, HubspotDeal } from "./types"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/app/shadcn_components/ui/tabs"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { BarChart2, Table2 } from "lucide-react"; +import TableViewer from "./TableViewer"; +import PropertyTable from "./PropertyTable"; +import PropertyDrawer from "./PropertyDrawer"; +import AnalyticsView from "./AnalyticsView"; +import type { + LiveTrackerProps, + TableModal, + ClassifiedDeal, + HubspotDeal, + DocumentDrawerState, +} from "./types"; export default function LiveTracker({ projects, totalDeals, majorConditionDeals, }: LiveTrackerProps) { - // UI State: which table modal is open - const [openTable, setOpenTable] = useState(null); + // ── Tab state ──────────────────────────────────────────────────────── + const [activeTab, setActiveTab] = useState<"analytics" | "properties">("analytics"); - // UI State: which project tab is selected + // ── Project selector (shared across both tabs) ─────────────────────── const projectCodes = projects.map((p) => p.projectCode); const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]); const currentProject = projects.find( (p) => p.projectCode === currentProjectCode ); - // Compute minor stuff inline (not data processing) - const majorIssues = majorConditionDeals.length; - const majorPercent = ((majorIssues / totalDeals) * 100).toFixed(1); - const hasSurveyData = (currentProject?.outcomePieSlices.length ?? 0) > 0; + // ── Drill-down table modal (used by AnalyticsView) ─────────────────── + const [openTable, setOpenTable] = useState(null); - // Group allDeals by outcome for pie chart click handler - const dealsByOutcome: Record = {}; - for (const deal of currentProject?.allDeals ?? []) { - if (deal.outcome) { - (dealsByOutcome[deal.outcome] ??= []).push(deal); - } - } + // ── Document drawer (used by PropertyTable) ────────────────────────── + const [drawerState, setDrawerState] = useState({ + open: false, + uprn: null, + dealname: null, + }); const handleOpenTable = ( stage: string, @@ -56,6 +66,10 @@ export default function LiveTracker({ }); }; + const handleOpenDrawer = (uprn: string | null, dealname: string | null) => { + setDrawerState({ open: true, uprn, dealname }); + }; + if (!totalDeals) { return ( @@ -67,123 +81,74 @@ export default function LiveTracker({ } return ( -
- {/* 🌍 Global Overview */} -
- {/* Project Selector */} - -
-

- Select Project -

-
- -
-
-
- - {/* Total Properties per Project */} - - handleOpenTable( - `${currentProjectCode} — All Properties`, - currentProject?.allDeals ?? [], - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ) - } - accent="brandblue" - /> - - {/* Major Issues */} - - handleOpenTable( - "Awaab's Law Reporting", - majorConditionDeals, - [ - "dealname", - "landlordPropertyId", - "majorConditionIssueDescription", - "majorConditionIssuePhotosS3", - ], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - majorConditionIssueDescription: "Surveyor's Notes", - majorConditionIssuePhotosS3: "Photo Evidence", - } - ) - } - accent={majorIssues > 0 ? "bright-red" : "red"} - /> -
- - {/* 📊 Project Insights */} - {currentProject && ( -
-
-

- Project-Level Insights —{" "} - {currentProjectCode} -

-
- -
+ setActiveTab(v as "analytics" | "properties")} + > + {/* Tab bar */} + + - - - + + Analytics + + + + Properties + + - {hasSurveyData && ( - - - + {/* Analytics tab */} + + {currentProject && ( + + )} + + + {/* Properties tab */} + +
+ {/* Project selector — mirrors analytics tab */} + {projects.length > 1 && ( +
+ Project: + +
)} -
-
- )} - {/* 🔹 Table Modal */} + +
+ + + + {/* ── Drill-down table modal ─────────────────────────────────────── */} {openTable && (
- {/* Breakdown Stats */} {openTable.breakdown && (
{Object.entries(openTable.breakdown).map(([category, items]) => { @@ -219,9 +183,7 @@ export default function LiveTracker({ const borderColor = isCompleted ? "border-brandblue/40" : "border-amber-200/50"; - const textColor = isCompleted - ? "text-brandblue" - : "text-amber-600"; + const textColor = isCompleted ? "text-brandblue" : "text-amber-600"; const labelColor = isCompleted ? "text-brandblue" : "text-amber-600/70"; @@ -231,20 +193,14 @@ export default function LiveTracker({ key={category} className={`${bgColor} rounded-lg p-3 border ${borderColor}`} > -

+

{category}

{items.length}

- {( - ((items.length / openTable.data.length) * 100) | - 0 - )} - % of total + {((items.length / openTable.data.length) * 100) | 0}% of total

); @@ -264,10 +220,7 @@ export default function LiveTracker({
)} + + {/* ── Document drawer ────────────────────────────────────────────── */} + + setDrawerState({ open: false, uprn: null, dealname: null }) + } + />
); } - -/** 🔸Small stat card component */ -function StatCard({ - icon: Icon, - title, - value, - subtitle, - onClick, - accent = "brandblue", -}: { - icon: any; - title: string; - value: string | number; - subtitle?: string; - onClick: () => void; - accent?: "brandblue" | "red" | "bright-red"; -}) { - const accentConfig = { - brandblue: { - gradient: "from-brandlightblue/30 to-brandlightblue/10", - border: "border-brandblue/20", - text: "text-brandblue", - value: "text-brandblue", - hover: "hover:border-brandblue/40 hover:shadow-lg", - icon: "text-brandblue", - }, - red: { - gradient: "from-red-100/30 to-red-50/20", - border: "border-red-300/40", - text: "text-red-500", - value: "text-red-500", - hover: "hover:border-red-300/60 hover:shadow-lg", - icon: "text-red-500", - }, - "bright-red": { - gradient: "from-red-100 to-red-50", - border: "border-red-500", - text: "text-red-700", - value: "text-red-900", - hover: "hover:border-red-600 hover:shadow-lg", - icon: "text-red-700", - }, - }; - - const config = accentConfig[accent]; - - return ( - -
-
-

- {title} -

-

- {value} - {subtitle && ( - - {subtitle} - - )} -

-
- -
-
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx index d745c4c1..36445711 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -1,339 +1,429 @@ "use client"; -import { Card } from "@tremor/react"; -import { AlertCircle } from "lucide-react"; import { motion } from "framer-motion"; -import ExpandableCountBar from "./ExpandableCountBar"; -import type { ProjectProgressData, ClassifiedDeal, HubspotDeal } from "./types"; +import { AlertCircle, CheckCircle2, ArrowRight, Users, PenLine, Hammer, FileCheck } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { STAGE_COLORS } from "./types"; +import type { ProjectProgressData, ClassifiedDeal, ClassifiedDeal as CD } from "./types"; +// ----------------------------------------------------------------------- +// Columns shown per card — enough context to understand WHY +// ----------------------------------------------------------------------- +const QUERIES_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "outcome", "coordinationStatus", "outcomeNotes", +]; +const QUERIES_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Reason / Stage", + outcome: "Survey Outcome", + coordinationStatus: "Coordination Status", + outcomeNotes: "Notes", +}; + +const COORD_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "coordinator", "coordinationStatus", "ioeV1Date", +]; +const COORD_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + coordinator: "Coordinator", + coordinationStatus: "Coordination Status", + ioeV1Date: "IOE/MTP Date", +}; + +const DESIGN_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "designer", "designStatus", "approvedPackage", "designDate", +]; +const DESIGN_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + designer: "Designer", + designStatus: "Design Status", + approvedPackage: "Package", + designDate: "Design Date", +}; + +const INSTALL_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "installer", "installerHandover", "actualMeasuresInstalled", +]; +const INSTALL_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + installer: "Installer", + installerHandover: "Handover Date", + actualMeasuresInstalled: "Measures Installed", +}; + +const LODGE_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "lodgementStatus", "postSapScore", "measuresLodgementDate", "fullLodgementDate", +]; +const LODGE_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + lodgementStatus: "Lodgement Status", + postSapScore: "Post-SAP Score", + measuresLodgementDate: "Measures Lodged", + fullLodgementDate: "Full Lodgement Date", +}; + +const EARLY_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "preSapScore", "outcome", "raStatus", +]; +const EARLY_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + preSapScore: "Pre-SAP Score", + outcome: "Survey Outcome", + raStatus: "Assessment Status", +}; + +// ----------------------------------------------------------------------- +// Circular progress ring (SVG) +// ----------------------------------------------------------------------- +function RingProgress({ pct, color = "#14163d", size = 80 }: { pct: number; color?: string; size?: number }) { + const r = 34; + const circ = 2 * Math.PI * r; + const offset = circ - (Math.min(pct, 100) / 100) * circ; + return ( + + + + + ); +} + +// ----------------------------------------------------------------------- +// Clickable phase summary card (coordination, design, install, lodgement) +// ----------------------------------------------------------------------- +function PhaseCard({ + icon: Icon, + title, + completedCount, + inProgressCount, + total, + completedPct, + inProgressPct, + ringColor, + onClick, +}: { + icon: React.ElementType; + title: string; + completedCount: number; + inProgressCount: number; + total: number; + completedPct: number; + inProgressPct: number; + ringColor: string; + onClick: () => void; +}) { + return ( + +
+
+
+
+ +
+ + {title} + +
+ +
+
+ Complete + {completedCount} +
+
+
+
+ {inProgressCount > 0 && ( + <> +
+ In progress + {inProgressCount} +
+
+
+
+ + )} +
+
+ +
+ +
+ {completedPct.toFixed(0)}% +
+
+
+ +
+ + View breakdown +
+ + ); +} + +// ----------------------------------------------------------------------- +// Main component +// ----------------------------------------------------------------------- interface ProgressOverviewProps { data: ProjectProgressData; onOpenTable?: ( stage: string, deals: ClassifiedDeal[], - columns?: (keyof HubspotDeal)[], - columnLabels?: Partial>, + columns?: (keyof CD)[], + columnLabels?: Partial>, breakdown?: Record ) => void; } -export default function ProgressOverview({ - data, - onOpenTable, -}: ProgressOverviewProps) { - // Pre-computed values from props +export default function ProgressOverview({ data, onOpenTable }: ProgressOverviewProps) { const { completedPercentage, completedCount, - totalDeals, + nonQueryTotal, queriesDeals, + stageProgress, coordination, design, + install, + lodgement, } = data; - // SVG circle calculations (pure, no memo needed) - const radius = 40; - const circumference = 2 * Math.PI * radius; - const strokeDashoffset = circumference - (completedPercentage / 100) * circumference; - - const handleCompletedClick = () => { - if (onOpenTable) { - onOpenTable( - "Completed Properties", - data.completedDeals, - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ); - } - }; - - const handleCoordinationClick = () => { - if (onOpenTable) { - const coordinationBreakdown = { - "Coordination Completed": coordination.completedDeals, - "Coordination in Progress": coordination.inProgressDeals, - }; - const allCoordDeals = [ - ...coordination.completedDeals, - ...coordination.inProgressDeals, - ]; - onOpenTable( - "Coordination Status", - allCoordDeals, - undefined, - undefined, - coordinationBreakdown - ); - } - }; - - const handleDesignClick = () => { - if (onOpenTable) { - const designBreakdown = { - "Design Completed": design.completedDeals, - "Design in Progress": design.inProgressDeals, - }; - const allDesignDeals = [ - ...design.completedDeals, - ...design.inProgressDeals, - ]; - onOpenTable( - "Design Status", - allDesignDeals, - undefined, - undefined, - designBreakdown - ); - } - }; - - const handleQueriesClick = () => { - if (onOpenTable && queriesDeals.length > 0) { - onOpenTable( - "Properties Needing Attention", - queriesDeals, - ["dealname", "landlordPropertyId", "coordinationStatus"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - coordinationStatus: "Issue", - } - ); - } - }; + // Early-stage rows (scope / booking / assessment) + const earlyStages = ["Scope & Planning", "Booking in Progress", "Assessment in Progress"]; + const earlyItems = stageProgress.filter((s) => earlyStages.includes(s.stage) && s.count > 0); return ( -
- {/* Work Completed - Full Width Overview at Top */} - - -
- {/* Header with Circular Progress */} -
-
-

- Work Completed -

-

- End-to-end project overview -

-
+ + - {/* Circular Progress */} -
- - {/* Background circle */} - - {/* Progress circle */} - - - - - - - - - {/* Center text */} -
- - {completedPercentage.toFixed(0)}% - - - {completedCount}/{totalDeals} - -
-
-
- - {/* CTA */} -
- View Completed Properties - -
-
-
-
- - {/* Early Stage Cards - Scope, Booking, Assessment */} - {(() => { - const earlyStages = [ - "Scope & Planning", - "Booking in Progress", - "Assessment in Progress", - ]; - const earlyStageItems = data.stageProgress.filter((s) => - earlyStages.includes(s.stage) - ); - - return earlyStageItems.length > 0 ? ( -
- {earlyStageItems.map((item) => ( - { - if (onOpenTable) { - onOpenTable( - item.stage, - item.deals, - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ); - } - }} - whileHover={{ scale: 1.02 }} - className="group relative text-left" - > - -
-
-

- {item.stage} -

-

- {item.count} -

-
- -
-

- {item.percentage.toFixed(0)}% of total -

-
- -
- View - -
-
-
-
- ))} -
- ) : null; - })()} - - {/* Project Summary Cards - Coordination & Design */} -
- - - -
- - {/* Queries / Attention Required Section */} - {queriesDeals.length > 0 && ( + {/* ── Completion header ──────────────────────────────────────────── */} + onOpenTable?.( + "Completed Properties", + data.completedDeals, + ["dealname", "landlordPropertyId", "displayStage", "actualMeasuresInstalled", "postSapScore", "fullLodgementDate"], + { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Stage", + actualMeasuresInstalled: "Measures Installed", + postSapScore: "Post-SAP", + fullLodgementDate: "Lodgement Date", + } + ) + } + className="group w-full text-left rounded-xl border border-emerald-200 bg-gradient-to-r from-emerald-50 to-white p-5 hover:border-emerald-300 hover:shadow-sm transition-all duration-200" > - -
- {/* Header with Alert */} -
-
- -
-
-

- Requires Your Input -

-

- These properties need your feedback or assistance to progress -

-
-
- - {/* Count Display */} -
-

- {queriesDeals.length} -

-

- {queriesDeals.length === 1 ? "property" : "properties"}{" "} - awaiting action -

-
- - {/* CTA */} -
- Review Details - +
+
+ +
+ + {completedPercentage.toFixed(0)}% +
- +
+
+ + Work Completed +
+

+ {completedCount} + / {nonQueryTotal} +

+

Properties fully lodged and funded

+
+ +
- )} -
+ + {/* ── Early stage chips ─────────────────────────────────────────── */} + {earlyItems.length > 0 && ( +
+ {earlyItems.map((item) => { + const c = STAGE_COLORS[item.stage]; + return ( + + onOpenTable?.(item.stage, item.deals, EARLY_COLUMNS, EARLY_LABELS) + } + className={`group text-left rounded-xl border p-3 transition-all duration-200 hover:shadow-sm ${c.bg} ${c.border} hover:opacity-90`} + > +
+ + + {item.stage} + +
+

{item.count}

+

+ {item.percentage.toFixed(0)}% of total +

+
+ ); + })} +
+ )} + + {/* ── Phase summary cards ──────────────────────────────────────── */} +
+ + onOpenTable?.( + "Coordination Status", + [...coordination.completedDeals, ...coordination.inProgressDeals], + COORD_COLUMNS, + COORD_LABELS, + { + "Coordination Complete": coordination.completedDeals, + "In Progress": coordination.inProgressDeals, + } + ) + } + /> + + onOpenTable?.( + "Design Status", + [...design.completedDeals, ...design.inProgressDeals], + DESIGN_COLUMNS, + DESIGN_LABELS, + { + "Design Complete": design.completedDeals, + "In Progress": design.inProgressDeals, + } + ) + } + /> + + onOpenTable?.( + "Installation Status", + [...install.completedDeals, ...install.inProgressDeals], + INSTALL_COLUMNS, + INSTALL_LABELS, + { + "Install Complete": install.completedDeals, + "In Progress": install.inProgressDeals, + } + ) + } + /> + + onOpenTable?.( + "Lodgement Status", + [...lodgement.completedDeals, ...lodgement.inProgressDeals], + LODGE_COLUMNS, + LODGE_LABELS, + { + "Fully Lodged": lodgement.completedDeals, + "In Progress": lodgement.inProgressDeals, + } + ) + } + /> +
+ + {/* ── Requires input ────────────────────────────────────────────── */} + {queriesDeals.length > 0 && ( + + onOpenTable?.( + "Properties Requiring Attention", + queriesDeals, + QUERIES_COLUMNS, + QUERIES_LABELS + ) + } + className="group w-full text-left rounded-xl border border-amber-300 bg-gradient-to-r from-amber-50 to-white p-4 hover:border-amber-400 hover:shadow-sm transition-all duration-200" + > +
+
+ +
+
+
+ Requires Your Input + + {queriesDeals.length} + +
+

+ Click to see the issue, survey outcome, and coordination status for each property +

+
+ +
+
+ )} + + ); } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx index 53b04a45..6a957e42 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -1,18 +1,16 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { redirect } from "next/navigation"; -import { surveyDB } from "../../../../../db/surveyDB/connection"; -import { hubspotDealData } from "../../../../../db/schema/crm/hubspot_deal_table"; -import { hubspotCompanyData } from "@/app/db/schema/crm/hubspot_company_table"; -import { eq } from "drizzle-orm"; import LiveTracker from "./LiveTracker"; import { computeLiveTrackerData } from "./transforms"; -import type { HubspotDeal } from "./types"; +// MOCK PHASE: using mock data while DB migration is in progress +// When migration lands, restore the DB query below and remove this import +import { MOCK_DEALS } from "./mockData"; export default async function LiveReportingPage(props: { params: Promise<{ slug: string }>; }) { - const { slug: portfolioId } = await props.params; + const { slug: _portfolioId } = await props.params; const user = await getServerSession(AuthOptions); if (!user?.user) { @@ -20,41 +18,13 @@ export default async function LiveReportingPage(props: { redirect("/"); } - // 🏢 Fetch the company - - const [company] = await surveyDB - .select() - .from(hubspotCompanyData) - .where(eq(hubspotCompanyData.groupId, portfolioId)); - - if (!company) { - return ( -
-
- No information to show. -
-
- ); - } - - // 💼 Fetch deals for that company - const deals = await surveyDB - .select() - .from(hubspotDealData) - .where(eq(hubspotDealData.companyId, company.companyId)); - - if (!deals || deals.length === 0) { - return ( -
-
- No information to show. -
-
- ); - } - - // 🔄 Transform raw deals to typed and computed data - const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); + // MOCK PHASE ↓↓↓ + // TODO: replace with real DB query once migration is applied: + // const [company] = await surveyDB.select().from(hubspotCompanyData).where(eq(hubspotCompanyData.groupId, portfolioId)); + // const deals = await surveyDB.select().from(hubspotDealData).where(eq(hubspotDealData.companyId, company.companyId)); + // const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); + const trackerData = computeLiveTrackerData(MOCK_DEALS); + // ↑↑↑ END MOCK PHASE return (
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts index 692b7ef5..4119cf6f 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -12,6 +12,8 @@ import type { OutcomeSlice, LiveTrackerProps, WorkPhaseStats, + DampMouldRiskData, + FunnelStage, } from "./types"; import { @@ -25,7 +27,7 @@ import { // ----------------------------------------------------------------------- const STAGE_ID_MAP: Record = { "1617223910": "Scope & Planning", //[Ops] Backlog - "3583836399": "Scope & Planning", //[Ops] Route Planning + "3583836399": "Scope & Planning", //[Ops] Route Planning "3589581001": "Booking in Progress", // [Bookings] Ready for Bookings Team "3569878239": "Booking in Progress", //[Bookings] Send initial booking SMS "1617223911": "Booking in Progress", // [Bookings] Send Email @@ -39,13 +41,13 @@ const STAGE_ID_MAP: Record = { "1617223913": "Assessment in Progress", //[Ops] Survey in Progress "2558220518": "Assessment in Progress", // [Ops] Not attempted - needs reallocation "3474594026": "Booking in Progress", //[Ops/Bookings] Rebooked - Needs updating - "3206388924": "Assessment in Progress", //[Ops] Surveyed - Pending Upload from Surveyor (Up to Khalim + Kev as debatable) + "3206388924": "Assessment in Progress", //[Ops] Surveyed - Pending Upload from Surveyor "1617223915": "Queries", //[Ops] No Access - Need Sign Off "1617223917": "Queries", //[Ops] No Access - No Revisit "1887735998": "Queries", //[Ops] Not Viable "3061261536": "Queries", //[Sales/Tech] Major condition issue "3948185842": "AFTER_ASSESSMENT", //[Admin] Admin to check all paperwork for external comms - "1617223914": "AFTER_ASSESSMENT",// [Ops]Surveyed in Pashub, Transit Job to Co-ordination + "1617223914": "AFTER_ASSESSMENT", // [Ops] Surveyed in Pashub, Transit Job to Co-ordination "1617223916": "Queries", // [Ops] Properties to Review Manually "2628341989": "Assessment in Progress", //[Ops] Assessment needs correction "3441170637": "AFTER_ASSESSMENT", //[Ops] Awaiting PV Design @@ -54,9 +56,6 @@ const STAGE_ID_MAP: Record = { "1960060104": "Queries", //[Ops] HA Informed "1960060105": "Queries", //[Ops] HA Works Scheduled "1960060106": "AFTER_ASSESSMENT", //[Ops] HA Works Complete - // "1668803772": "", //[Ops] ERF Delivered to HA - // "1668803773": "", //[Ops] ERF Signed - // "2769407183": "", //[Ops] PV - Needs Heating Upgrade (Pre EPR D) "2769407184": "Queries", //[Ops] Talk to client, Needs Heating Upgrade (Pre EPR C) "2702650617": "AFTER_ASSESSMENT", //[Design] Ready for Design "2473886962": "AFTER_ASSESSMENT", //[Design] Design in progress @@ -65,12 +64,12 @@ const STAGE_ID_MAP: Record = { // ----------------------------------------------------------------------- // After-assessment sub-classification -// Resolves AFTER_ASSESSMENT deals based on coordinationStatus and designStatus +// Resolves AFTER_ASSESSMENT deals based on coordinationStatus + designStatus // ----------------------------------------------------------------------- function resolveAfterAssessmentStage( coordinationStatus: string | null, designStatus: string | null -): DisplayStage { +): "Coordination in Progress" | "Design in Progress" | "POST_DESIGN" | "Queries" { const coord = coordinationStatus?.toUpperCase() ?? ""; const design = designStatus?.toUpperCase() ?? ""; @@ -83,25 +82,42 @@ function resolveAfterAssessmentStage( coord.includes("(V2) IOE/MTP COMPLETE") || coord.includes("(V3) IOE/MTP COMPLETE") ) { - return design === "UPLOADED" ? "Completed" : "Design in Progress"; + return design === "UPLOADED" ? "POST_DESIGN" : "Design in Progress"; } // Default for AFTER_ASSESSMENT return "Coordination in Progress"; } +// ----------------------------------------------------------------------- +// Post-design sub-classification +// Called when design is UPLOADED — resolves install / lodgement / completed +// ----------------------------------------------------------------------- +function resolvePostDesignStage(deal: HubspotDeal): DisplayStage { + if (deal.fullLodgementDate) return "Completed"; + if (deal.lodgementStatus) return "Lodgement"; + if (deal.actualMeasuresInstalled || deal.installerHandover) return "Installation Complete"; + return "Awaiting Install"; +} + // ----------------------------------------------------------------------- // Resolve display stage for a single deal -// Maps dealstage ID + coordinationStatus + designStatus -> DisplayStage +// Maps dealstage ID + coordination/design/install status -> DisplayStage // ----------------------------------------------------------------------- export function resolveDisplayStage(deal: HubspotDeal): DisplayStage { const raw = STAGE_ID_MAP[deal.dealstage ?? ""] ?? "AFTER_ASSESSMENT"; if (raw === "AFTER_ASSESSMENT") { - return resolveAfterAssessmentStage( + const afterAssessment = resolveAfterAssessmentStage( deal.coordinationStatus, deal.designStatus ); + + if (afterAssessment === "POST_DESIGN") { + return resolvePostDesignStage(deal); + } + + return afterAssessment; } // RA ISSUE override can apply to other stages too @@ -125,6 +141,52 @@ export function classifyDeals(deals: HubspotDeal[]): ClassifiedDeal[] { })); } +// ----------------------------------------------------------------------- +// Compute damp & mould risk — survey vs coordination stage comparison +// ----------------------------------------------------------------------- +export function computeDampMouldRisk(deals: ClassifiedDeal[]): DampMouldRiskData { + const surveyFlagDeals = deals.filter((d) => !!d.majorConditionIssuePhotosS3); + const coordinatorFlagDeals = deals.filter((d) => !!d.dampMouldFlag); + const bothFlaggedCount = surveyFlagDeals.filter((d) => !!d.dampMouldFlag).length; + + return { + surveyFlagCount: surveyFlagDeals.length, + coordinatorFlagCount: coordinatorFlagDeals.length, + bothFlaggedCount, + totalDeals: deals.length, + surveyFlagDeals, + coordinatorFlagDeals, + }; +} + +// ----------------------------------------------------------------------- +// Compute pipeline funnel — dual counts (current snapshot + cumulative) +// ----------------------------------------------------------------------- +export function computeFunnelStages(deals: ClassifiedDeal[]): FunnelStage[] { + const nonQueryDeals = deals.filter((d) => d.displayStage !== "Queries"); + const total = nonQueryDeals.length; + + return STAGE_ORDER.map((stage) => { + const stageIndex = STAGE_ORDER.indexOf(stage); + + const currentCount = nonQueryDeals.filter( + (d) => d.displayStage === stage + ).length; + + const cumulativeCount = nonQueryDeals.filter( + (d) => STAGE_ORDER.indexOf(d.displayStage) >= stageIndex + ).length; + + return { + stage, + currentCount, + currentPct: total > 0 ? (currentCount / total) * 100 : 0, + cumulativeCount, + cumulativePct: total > 0 ? (cumulativeCount / total) * 100 : 0, + }; + }); +} + // ----------------------------------------------------------------------- // Compute all ProjectProgressData for a set of already-classified deals // ----------------------------------------------------------------------- @@ -162,12 +224,16 @@ export function computeProjectProgress( const totalDeals = deals.length; // Coordination phase: - // completed = Design in Progress + Completed (i.e. coordination is done) + // completed = Design in Progress + Awaiting Install + Installation Complete + Lodgement + Completed // in progress = Coordination in Progress - const coordCompletedDeals = deals.filter( - (d) => - d.displayStage === "Design in Progress" || - d.displayStage === "Completed" + const coordCompletedDeals = deals.filter((d) => + [ + "Design in Progress", + "Awaiting Install", + "Installation Complete", + "Lodgement", + "Completed", + ].includes(d.displayStage) ); const coordInProgressDeals = deals.filter( (d) => d.displayStage === "Coordination in Progress" @@ -179,33 +245,78 @@ export function computeProjectProgress( completedCount: coordCompletedDeals.length, inProgressCount: coordInProgressDeals.length, completedPercentage: - totalDeals > 0 - ? (coordCompletedDeals.length / totalDeals) * 100 - : 0, + totalDeals > 0 ? (coordCompletedDeals.length / totalDeals) * 100 : 0, inProgressPercentage: - totalDeals > 0 - ? (coordInProgressDeals.length / totalDeals) * 100 - : 0, + totalDeals > 0 ? (coordInProgressDeals.length / totalDeals) * 100 : 0, total: totalDeals, }; // Design phase: - // completed = Completed stage + // completed = Awaiting Install + Installation Complete + Lodgement + Completed // in progress = Design in Progress + const designCompletedDeals = deals.filter((d) => + [ + "Awaiting Install", + "Installation Complete", + "Lodgement", + "Completed", + ].includes(d.displayStage) + ); const designInProgressDeals = deals.filter( (d) => d.displayStage === "Design in Progress" ); const design: WorkPhaseStats = { - completedDeals, + completedDeals: designCompletedDeals, inProgressDeals: designInProgressDeals, - completedCount, + completedCount: designCompletedDeals.length, inProgressCount: designInProgressDeals.length, + completedPercentage: + totalDeals > 0 ? (designCompletedDeals.length / totalDeals) * 100 : 0, + inProgressPercentage: + totalDeals > 0 ? (designInProgressDeals.length / totalDeals) * 100 : 0, + total: totalDeals, + }; + + // Install phase: + // completed = Lodgement + Completed + // in progress = Installation Complete + const installCompletedDeals = deals.filter((d) => + ["Lodgement", "Completed"].includes(d.displayStage) + ); + const installInProgressDeals = deals.filter( + (d) => d.displayStage === "Installation Complete" + ); + + const install: WorkPhaseStats = { + completedDeals: installCompletedDeals, + inProgressDeals: installInProgressDeals, + completedCount: installCompletedDeals.length, + inProgressCount: installInProgressDeals.length, + completedPercentage: + totalDeals > 0 ? (installCompletedDeals.length / totalDeals) * 100 : 0, + inProgressPercentage: + totalDeals > 0 ? (installInProgressDeals.length / totalDeals) * 100 : 0, + total: totalDeals, + }; + + // Lodgement phase: + // completed = Completed + // in progress = Lodgement + const lodgementInProgressDeals = deals.filter( + (d) => d.displayStage === "Lodgement" + ); + + const lodgement: WorkPhaseStats = { + completedDeals, + inProgressDeals: lodgementInProgressDeals, + completedCount, + inProgressCount: lodgementInProgressDeals.length, completedPercentage: totalDeals > 0 ? (completedCount / totalDeals) * 100 : 0, inProgressPercentage: totalDeals > 0 - ? (designInProgressDeals.length / totalDeals) * 100 + ? (lodgementInProgressDeals.length / totalDeals) * 100 : 0, total: totalDeals, }; @@ -220,6 +331,10 @@ export function computeProjectProgress( totalDeals, coordination, design, + install, + lodgement, + dampMouldRisk: computeDampMouldRisk(deals), + funnelStages: computeFunnelStages(deals), }; } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts index 229a2ede..8b0ae9ad 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -5,6 +5,7 @@ // ----------------------------------------------------------------------- // Raw DB row from hubspotDealData table +// New CRM-synced fields are nullable — populated by HubSpot sync // ----------------------------------------------------------------------- export type HubspotDeal = { id: string; @@ -22,12 +23,46 @@ export type HubspotDeal = { majorConditionIssuePhotosS3: string | null; coordinationStatus: string | null; designStatus: string | null; + + // ── CRM-synced additions ────────────────────────────────────────────── + pashubLink: string | null; + sharepointLink: string | null; + dampMouldFlag: string | null; // coordinator-stage damp/mould flag + preSapScore: string | null; // kept as text (HubSpot returns strings) + coordinator: string | null; + ioeV1Date: Date | null; + ioeV2Date: Date | null; + ioeV3Date: Date | null; + proposedMeasures: string | null; + approvedPackage: string | null; + designer: string | null; + designDate: Date | null; + actualMeasuresInstalled: string | null; + installer: string | null; + installerHandover: string | null; + postSapScore: string | null; + lodgementStatus: string | null; + measuresLodgementDate: Date | null; + fullLodgementDate: Date | null; + + // ── Internally tracked stubs (no HubSpot source yet) ───────────────── + raDateBooking: Date | null; + raDateActual: Date | null; + raStatus: string | null; + postEpcDateBooking: Date | null; + postEpcDateActual: Date | null; + postEpcStatus: string | null; + postEprDate: Date | null; + postEprStatus: string | null; + createdAt: Date; updatedAt: Date; }; // ----------------------------------------------------------------------- -// Stage classification result - human-readable display labels +// Stage classification result — human-readable display labels +// Full end-to-end pipeline: assessment → coordination → design → +// install → lodgement → completed (funded) // ----------------------------------------------------------------------- export type DisplayStage = | "Scope & Planning" @@ -35,12 +70,15 @@ export type DisplayStage = | "Assessment in Progress" | "Coordination in Progress" | "Design in Progress" + | "Awaiting Install" + | "Installation Complete" + | "Lodgement" | "Completed" | "Queries" | "Unknown Stage"; // ----------------------------------------------------------------------- -// A classified deal - original row plus its resolved display stage +// A classified deal — original row plus its resolved display stage // ----------------------------------------------------------------------- export type ClassifiedDeal = HubspotDeal & { displayStage: DisplayStage; @@ -57,7 +95,7 @@ export type StageProgressItem = { }; // ----------------------------------------------------------------------- -// Coordination/Design summary card data +// Coordination/Design/Install/Lodgement summary card data // ----------------------------------------------------------------------- export type WorkPhaseStats = { completedDeals: ClassifiedDeal[]; @@ -69,6 +107,29 @@ export type WorkPhaseStats = { total: number; }; +// ----------------------------------------------------------------------- +// Damp & mould risk comparison (survey-stage vs coordination-stage flags) +// ----------------------------------------------------------------------- +export type DampMouldRiskData = { + surveyFlagCount: number; // majorConditionIssuePhotosS3 not null + coordinatorFlagCount: number; // dampMouldFlag not null/non-empty + bothFlaggedCount: number; // flagged at both stages (highest risk) + totalDeals: number; + surveyFlagDeals: ClassifiedDeal[]; + coordinatorFlagDeals: ClassifiedDeal[]; +}; + +// ----------------------------------------------------------------------- +// Pipeline funnel data — dual counts per stage +// ----------------------------------------------------------------------- +export type FunnelStage = { + stage: DisplayStage; + currentCount: number; // deals at exactly this stage right now + currentPct: number; // as % of non-query total + cumulativeCount: number; // deals that have reached this stage or beyond + cumulativePct: number; +}; + // ----------------------------------------------------------------------- // All computed data for the ProgressOverview component // ----------------------------------------------------------------------- @@ -82,6 +143,10 @@ export type ProjectProgressData = { totalDeals: number; coordination: WorkPhaseStats; design: WorkPhaseStats; + install: WorkPhaseStats; + lodgement: WorkPhaseStats; + dampMouldRisk: DampMouldRiskData; + funnelStages: FunnelStage[]; }; // ----------------------------------------------------------------------- @@ -114,15 +179,34 @@ export type LiveTrackerProps = { // ----------------------------------------------------------------------- // Table drill-down shape (stays in LiveTracker state) +// columns can include computed ClassifiedDeal fields (e.g. displayStage) // ----------------------------------------------------------------------- export type TableModal = { stage: string; data: ClassifiedDeal[]; - columns: (keyof HubspotDeal)[]; - columnLabels: Partial>; + columns: (keyof ClassifiedDeal)[]; + columnLabels: Partial>; breakdown?: Record; }; +// ----------------------------------------------------------------------- +// Document drawer types +// ----------------------------------------------------------------------- +export type PropertyDocument = { + id: string; + s3FileUri: string; + s3JsonUri: string | null; + docType: string; + s3FileUploadTimestamp: string; // ISO string + uprn: string; +}; + +export type DocumentDrawerState = { + open: boolean; + uprn: string | null; + dealname: string | null; +}; + // ----------------------------------------------------------------------- // Surveyor outcome constants (single source of truth) // ----------------------------------------------------------------------- @@ -149,5 +233,83 @@ export const STAGE_ORDER: DisplayStage[] = [ "Assessment in Progress", "Coordination in Progress", "Design in Progress", + "Awaiting Install", + "Installation Complete", + "Lodgement", "Completed", ]; + +// ----------------------------------------------------------------------- +// Stage colour mapping — used for badges (PropertyTable) and funnel bars (AnalyticsView) +// ----------------------------------------------------------------------- +export const STAGE_COLORS: Record< + DisplayStage, + { bg: string; text: string; border: string; dot: string } +> = { + "Scope & Planning": { + bg: "bg-slate-100", + text: "text-slate-700", + border: "border-slate-200", + dot: "bg-slate-400", + }, + "Booking in Progress": { + bg: "bg-sky-50", + text: "text-sky-700", + border: "border-sky-200", + dot: "bg-sky-400", + }, + "Assessment in Progress": { + bg: "bg-violet-50", + text: "text-violet-700", + border: "border-violet-200", + dot: "bg-violet-400", + }, + "Coordination in Progress": { + bg: "bg-amber-50", + text: "text-amber-700", + border: "border-amber-200", + dot: "bg-amber-400", + }, + "Design in Progress": { + bg: "bg-orange-50", + text: "text-orange-700", + border: "border-orange-200", + dot: "bg-orange-400", + }, + "Awaiting Install": { + bg: "bg-purple-50", + text: "text-purple-700", + border: "border-purple-200", + dot: "bg-purple-400", + }, + "Installation Complete": { + bg: "bg-teal-50", + text: "text-teal-700", + border: "border-teal-200", + dot: "bg-teal-400", + }, + Lodgement: { + bg: "bg-cyan-50", + text: "text-cyan-700", + border: "border-cyan-200", + dot: "bg-cyan-400", + }, + Completed: { + bg: "bg-emerald-50", + text: "text-emerald-700", + border: "border-emerald-200", + dot: "bg-emerald-500", + }, + Queries: { + bg: "bg-red-50", + text: "text-red-600", + border: "border-red-200", + dot: "bg-red-400", + }, + "Unknown Stage": { + bg: "bg-gray-50", + text: "text-gray-500", + border: "border-gray-100", + dot: "bg-gray-300", + }, +};