From 9923e5ee1361d80bd8e580fb7f3f71ee309c7ac6 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 25 Feb 2026 12:32:21 +0000 Subject: [PATCH 1/5] save working version of summary information --- .../your-projects/live/DealStageChart.tsx | 385 +++++++++--------- 1 file changed, 193 insertions(+), 192 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx index 082ff27f..3851c521 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx @@ -1,11 +1,15 @@ "use client"; -import { useMemo } from "react"; import { BarList, Card, Title } from "@tremor/react"; +import ExpandableCountBar from "./ExpandableCountBar"; + +/* ================================ + STAGE ORDER +================================ */ const STAGE_ORDER = [ "Initial planning", - "Booking Team to contact tenant", + "Booking team to contact tenant", "In Assessment", "In Coordination", "In Design", @@ -13,109 +17,62 @@ const STAGE_ORDER = [ "Queries", ]; -const stage = (label: string) => STAGE_ORDER.find((s) => s === label)!; +const stage = (label: string) => + STAGE_ORDER.find((s) => s === label)!; + +/* ================================ + AFTER ASSESSMENT LOGIC +================================ */ -// 🔧 Helper function to determine stage label after assessment based on coordination and design status const getAfterAssessmentLabel = ( coordinationStatus?: string, designStatus?: string ): string => { - // Normalize strings to uppercase for case-insensitive comparison const coordStatusUpper = coordinationStatus?.toUpperCase() ?? ""; const designStatusUpper = designStatus?.toUpperCase() ?? ""; - // 1. If coordination status is 'ra issue', return to 'queries' - if (coordStatusUpper === "RA ISSUE") { - return "Queries"; - } + if (coordStatusUpper === "RA ISSUE") return "Queries"; - // 2. If coordination status contains v1/v2/v3 ioe/mtp completed, show as 'In Design' if ( coordStatusUpper.includes("(V1) IOE/MTP COMPLETE") || coordStatusUpper.includes("(V2) IOE/MTP COMPLETE") || coordStatusUpper.includes("(V3) IOE/MTP COMPLETE") ) { - // 3. If design status is 'Uploaded', show as 'Completed' - if (designStatusUpper === "UPLOADED") { - return "Completed"; - } - // Otherwise show as 'In Design' + if (designStatusUpper === "UPLOADED") return "Completed"; return "In Design"; } - // Default to 'In Coordination' return "In Coordination"; }; -// 🏷️ Deal stage → display stage mapping +/* ================================ + STAGE LABELS +================================ */ + const STAGE_LABELS: Record = { - "1617223910": stage("Initial planning"), // 0 - [Ops] Backlog - "3583836399": stage("Initial planning"), // 0 - [Ops] Route Planning + "1617223910": stage("Initial planning"), + "3583836399": stage("Initial planning"), + "3589581001": stage("Booking team to contact tenant"), + "1984401629": stage("In Assessment"), - "3589581001": stage("Booking team to contact tenant"), // 1 - [Bookings] Ready for Bookings Team - "3569878239": stage("Booking team to contact tenant"), // 1 - [Bookings] Send initial booking SMS - "1617223911": stage("Booking team to contact tenant"), // 1 - [Bookings] Send Email - "1984184569": stage("Booking team to contact tenant"), // 1 - [Bookings] Phone booking - "3569572028": stage("Booking team to contact tenant"), // 1 - [Bookings] Preferences received from Tenant - "3570936026": stage("Booking team to contact tenant"), // 1 - [Bookings] Send Confirmation Comms - "2663668937": stage("Queries"), // 4 - [Bookings/Sales] Booking issues - needs HA support (Check with Aidan) - "1984401629": stage("In Assessment"), // 2 - [Bookings/Ops/Sales] No Contact Details - Ready for Route - "2558220518": stage("Booking team to contact tenant"), // 1 - [Ops] Not attempted - needs reallocation - "3474594026": stage("Booking team to contact tenant"), // 1 - [Ops/Bookings] Rebooked - Needs updating + "2628233422": "AFTER_ASSESSMENT", + "2702650617": "AFTER_ASSESSMENT", + "2473886962": "AFTER_ASSESSMENT", + "1668803774": "AFTER_ASSESSMENT", - "1617223912": stage("In Assessment"), // 2 - [Ops] Ready for Assignment to Route - "1617223913": stage("In Assessment"), // 2 - [Ops] Survey in Progress - "3206388924": stage("In Assessment"), // 2 - [Ops] Surveyed - Pending Upload from Surveyor - "1617223915": stage("In Assessment"), // 2 - [Ops] No Access - Need Sign Off - "1617223917": stage("Queries"), // 3 - [Ops] No Access - No Revisit - "2571417798": stage("Booking team to contact tenant"), // 1 - [Ops] Surveyed under 2019 - Needs Re-survey - - "1617223916": stage("In Assessment"), // 5 - [Ops] Properties to Review Manually - - // 🔧 ===== AFTER ASSESSMENT - Determine exact stage using coordination/design status logic ===== - // These are special internal stages that will be processed by getAfterAssessmentLabel - // and mapped to their final display stages ("In Coordination", "In Design", "Completed") - "2628341989": "AFTER_ASSESSMENT", // 5 - [Ops] Assessment needs correction - "3441170637": "AFTER_ASSESSMENT", // 5 - [Ops] Awaiting PV Design - - "1617223914": "AFTER_ASSESSMENT", // 5 - [Ops] Surveyed in Pashub, Transit Job to Co-ordination - "2628233422": "AFTER_ASSESSMENT", // 5 - [Coordination] Ready for coordination - "2702650617": "AFTER_ASSESSMENT", // 5 - [Design] Ready for Design - "2473886962": "AFTER_ASSESSMENT", // 5 - [Design] Design in progress - - "1668803774": "AFTER_ASSESSMENT", // 6 - [Finance] Ready for Invoicing - "3440363736": "AFTER_ASSESSMENT", // 6 - [Finance] Needs Invoicing - Files Sent - - // 🔧 Exception stages (handled separately) - "1887735998": stage("Queries"), // 3 - [Ops] Not Viable - "3061261536": stage("Queries"), // 4 - [Sales/Tech] Major condition issue - "1887735999": stage("Queries"), // 4 - [Ops] Needs HA Works - "3016601828": stage("Queries"), // 4 - [Engagement Team] EPC C Before Works - "2769407183": stage("Queries"), // 4 - [Ops] PV - Needs Heating Upgrade (Pre EPR D) + "1887735998": stage("Queries"), }; -// 🧩 Reasons for exception stages (HA support / Not viable) -const STAGE_REASONS: Record = { - // ---- Needs support from HA ---- - "2663668937": "Booking issues due to tenant difficulties.", - "3061261536": "Awaab's Law", - "1887735999": "", - "3016601828": "RA is currently EPR C. Convert to EPC?", - "2769407183": "Needs HA heating upgrade. Domna/HA discussion required.", +/* ================================ + TYPES +================================ */ - // ---- Not viable for funding ---- - "1617223917": "", - "1887735998": "", -}; - -// ✅ Define an explicit Deal type for clarity interface Deal { dealname: string; landlordPropertyId: string; dealstage: string; coordinationStatus?: string; designStatus?: string; - reason?: string; [key: string]: any; } @@ -123,124 +80,167 @@ interface DealStageChartProps { deals: Deal[]; onOpenTable?: ( stageName: string, - filteredDeals: Deal[], - columns?: string[], - columnLabels?: { [key: string]: string } + filteredDeals: Deal[] ) => void; } -export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) { - const data = useMemo(() => { - const counts: Record = {}; +/* ================================ + STAGE RESOLUTION ENGINE +================================ */ - deals.forEach((d) => { - const stageId = d.dealstage || "unknown"; - let stageName = STAGE_LABELS[stageId] || "Unknown Stage"; +const resolveDisplayStage = (deal: Deal): string => { + let stageName = STAGE_LABELS[deal.dealstage] || "Unknown Stage"; - // 🔧 For deals marked as "AFTER_ASSESSMENT", determine exact stage using coordination/design status logic - if (stageName === "AFTER_ASSESSMENT") { - const label = getAfterAssessmentLabel(d.coordinationStatus, d.designStatus); - stageName = label || "In Coordination"; // Default to "In Coordination" if no label returned - } + if (stageName === "AFTER_ASSESSMENT") { + stageName = + getAfterAssessmentLabel( + deal.coordinationStatus, + deal.designStatus + ) || "In Coordination"; + } - // 🔧 For "Initial Planning" deals, check if coordination status is 'RA ISSUE' - if (stageName === "Initial planning") { - const coordStatusUpper = d.coordinationStatus?.toUpperCase() ?? ""; - if (coordStatusUpper === "RA ISSUE") { - stageName = "Queries"; - } - } + if (stageName === "Initial planning") { + const coordStatusUpper = + deal.coordinationStatus?.toUpperCase() ?? ""; + if (coordStatusUpper === "RA ISSUE") { + stageName = "Queries"; + } + } - counts[stageName] = (counts[stageName] || 0) + 1; - }); + return stageName; +}; - return STAGE_ORDER.map((name) => ({ - name, - value: counts[name] || 0, - })); - }, [deals]); +/* ================================ + GENERIC STAGE FILTER +================================ */ + +const getDealsByResolvedStage = ( + deals: Deal[], + stages: string[] +): Deal[] => { + return deals.filter((deal) => + stages.includes(resolveDisplayStage(deal)) + ); +}; + +/* ================================ + COMPONENT +================================ */ + +export function DealStageChart({ + deals, + onOpenTable, +}: DealStageChartProps) { + + /* ---------- Build Chart Data ---------- */ + + const counts: Record = {}; + + deals.forEach((deal) => { + const stage = resolveDisplayStage(deal); + counts[stage] = (counts[stage] || 0) + 1; + }); + + const data = STAGE_ORDER.map((name) => ({ + name, + value: counts[name] || 0, + })); + + /* ---------- Summary Buckets ---------- */ + + const coordinationCompletedDeals = getDealsByResolvedStage( + deals, + ["In Design", "Completed"] + ); + + const designCompletedDeals = getDealsByResolvedStage( + deals, + ["Completed"] + ); const total = deals.length; - const handleBarClick = (value: { name: string; value: number }) => { - const filteredDeals: Deal[] = deals - .filter((d) => { - let stageName = STAGE_LABELS[d.dealstage] || "Unknown Stage"; + /* ---------- Shared Summary Click Handler ---------- */ + const handleSummaryClick = ( + label: string, + stages: string[] +) => { + const filteredDeals = getDealsByResolvedStage( + deals, + stages + ); - // 🔧 For deals marked as "AFTER_ASSESSMENT", determine exact stage using coordination/design status logic - if (stageName === "AFTER_ASSESSMENT") { - const label = getAfterAssessmentLabel(d.coordinationStatus, d.designStatus); - stageName = label || "In Coordination"; // Default to "In Coordination" if no label returned - } + onOpenTable?.(label, filteredDeals); +}; - // 🔧 For "Initial Planning" deals, check if coordination status is 'RA ISSUE' - if (stageName === "Initial planning") { - const coordStatusUpper = d.coordinationStatus?.toUpperCase() ?? ""; - if (coordStatusUpper === "RA ISSUE") { - stageName = "Queries"; - } - } - return stageName === value.name; - }) - .map((d) => ({ - ...d, - // ✅ Always provide a string to avoid undefined issues - reason: STAGE_REASONS[d.dealstage] ?? "", - })); + /* ---------- Stage Bar Click ---------- */ - const isException = - value.name === "Needs support from HA" || - value.name === "Not viable for funding"; + const handleBarClick = (value: { + name: string; + value: number; + }) => { + const filteredDeals = getDealsByResolvedStage( + deals, + [value.name] + ); - // Add "Reason" column if it's an exception stage - const columns = isException - ? ["dealname", "landlordPropertyId", "reason"] - : ["dealname", "landlordPropertyId"]; - - const columnLabels = isException - ? { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - reason: "Reason", - } - : { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - }; - - // ✅ Explicit cast ensures no type mismatch - onOpenTable?.(value.name, filteredDeals, columns, columnLabels as Record); + onOpenTable?.(value.name, filteredDeals); }; - // Split into normal + exception stages + /* ---------- Split Normal vs Exception ---------- */ + const normalStages = data.filter( - (d) => - !["Queries"].includes(d.name) && - d.name !== "" + (d) => d.name !== "Queries" ); - const exceptionStages = data.filter((d) => - ["Queries"].includes(d.name) + const exceptionStages = data.filter( + (d) => d.name === "Queries" ); -return ( -
- {/* ✅ Main Progress Chart */} - -
- - Project Progress by Stage - -

- Click a bar to view related properties -

-

- Total: {total.toLocaleString()} properties -

-
+ return ( +
+ + {/* ===== Summary Panels ===== */} + + + + handleSummaryClick( + "Coordination Completed", + ["In Design", "Completed"] + ) + } + /> + + + handleSummaryClick( + "Design Completed", + ["Completed"] + ) + } + /> + + {/* ===== Main Progress Chart ===== */} + + +
+ + Project Progress by Stage + +

+ Click a bar to view related properties +

+

+ Total: {total.toLocaleString()} properties +

+
-
-
-
+ - {/* 🚨 Exception Chart */} - -
- - Needs HA Support & Not Viable - -

- Click to explore exception properties (reasons appear in table) -

-
+ {/* ===== Queries / Exception Chart ===== */} -
- -
-
-
-); -} + +
+ + Needs HA Support & Not Viable + +

+ Properties requiring attention +

+
+ +
+ +
+
+ +
+ ); +} \ No newline at end of file From b1ce906f2f66ef08c1c7a0adb099992231459b34 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 25 Feb 2026 12:33:43 +0000 Subject: [PATCH 2/5] added new file --- .../your-projects/live/ExpandableCountBar.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx new file mode 100644 index 00000000..9da2771e --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx @@ -0,0 +1,34 @@ +"use client" + +interface ExpandableCountBarProps { + title: string + items: T[] + onClick?: (items: T[]) => void + className?: string +} + +export default function ExpandableCountBar({ + title, + items, + onClick, + className = "", +}: ExpandableCountBarProps) { + + const count = items.length + + return ( +
onClick?.(items)} + className={`w-full cursor-pointer rounded-xl border bg-white shadow-sm hover:shadow-md transition-all duration-200 p-5 flex justify-between items-center ${className}`} + > +
+ {title} +
+ +
+ {count} items + +
+
+ ) +} \ No newline at end of file From 694871a36b4c12c0bd68a9d9be0cc40e5d8869b5 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 25 Feb 2026 14:53:51 +0000 Subject: [PATCH 3/5] show demo to harry --- .../your-projects/live/DealStageChart.tsx | 257 +------------ .../your-projects/live/ExpandableCountBar.tsx | 106 +++++- .../your-projects/live/ProgressOverview.tsx | 347 ++++++++++++++++++ .../(portfolio)/your-projects/live/Report.tsx | 179 +++++---- .../live/SurveyedResultsPieChart.tsx | 40 +- .../your-projects/live/TableViewer.tsx | 24 +- 6 files changed, 590 insertions(+), 363 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx index 3851c521..3bf3e0f8 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx @@ -1,71 +1,6 @@ "use client"; -import { BarList, Card, Title } from "@tremor/react"; -import ExpandableCountBar from "./ExpandableCountBar"; - -/* ================================ - STAGE ORDER -================================ */ - -const STAGE_ORDER = [ - "Initial planning", - "Booking team to contact tenant", - "In Assessment", - "In Coordination", - "In Design", - "Completed", - "Queries", -]; - -const stage = (label: string) => - STAGE_ORDER.find((s) => s === label)!; - -/* ================================ - AFTER ASSESSMENT LOGIC -================================ */ - -const getAfterAssessmentLabel = ( - coordinationStatus?: string, - designStatus?: string -): string => { - const coordStatusUpper = coordinationStatus?.toUpperCase() ?? ""; - const designStatusUpper = designStatus?.toUpperCase() ?? ""; - - if (coordStatusUpper === "RA ISSUE") return "Queries"; - - if ( - coordStatusUpper.includes("(V1) IOE/MTP COMPLETE") || - coordStatusUpper.includes("(V2) IOE/MTP COMPLETE") || - coordStatusUpper.includes("(V3) IOE/MTP COMPLETE") - ) { - if (designStatusUpper === "UPLOADED") return "Completed"; - return "In Design"; - } - - return "In Coordination"; -}; - -/* ================================ - STAGE LABELS -================================ */ - -const STAGE_LABELS: Record = { - "1617223910": stage("Initial planning"), - "3583836399": stage("Initial planning"), - "3589581001": stage("Booking team to contact tenant"), - "1984401629": stage("In Assessment"), - - "2628233422": "AFTER_ASSESSMENT", - "2702650617": "AFTER_ASSESSMENT", - "2473886962": "AFTER_ASSESSMENT", - "1668803774": "AFTER_ASSESSMENT", - - "1887735998": stage("Queries"), -}; - -/* ================================ - TYPES -================================ */ +import ProgressOverview from "./ProgressOverview"; interface Deal { dealname: string; @@ -80,199 +15,19 @@ interface DealStageChartProps { deals: Deal[]; onOpenTable?: ( stageName: string, - filteredDeals: Deal[] + filteredDeals: Deal[], + columns?: string[], + columnLabels?: Record, + breakdown?: Record ) => void; } -/* ================================ - STAGE RESOLUTION ENGINE -================================ */ - -const resolveDisplayStage = (deal: Deal): string => { - let stageName = STAGE_LABELS[deal.dealstage] || "Unknown Stage"; - - if (stageName === "AFTER_ASSESSMENT") { - stageName = - getAfterAssessmentLabel( - deal.coordinationStatus, - deal.designStatus - ) || "In Coordination"; - } - - if (stageName === "Initial planning") { - const coordStatusUpper = - deal.coordinationStatus?.toUpperCase() ?? ""; - if (coordStatusUpper === "RA ISSUE") { - stageName = "Queries"; - } - } - - return stageName; -}; - -/* ================================ - GENERIC STAGE FILTER -================================ */ - -const getDealsByResolvedStage = ( - deals: Deal[], - stages: string[] -): Deal[] => { - return deals.filter((deal) => - stages.includes(resolveDisplayStage(deal)) - ); -}; - -/* ================================ - COMPONENT -================================ */ - export function DealStageChart({ deals, onOpenTable, }: DealStageChartProps) { - /* ---------- Build Chart Data ---------- */ - - const counts: Record = {}; - - deals.forEach((deal) => { - const stage = resolveDisplayStage(deal); - counts[stage] = (counts[stage] || 0) + 1; - }); - - const data = STAGE_ORDER.map((name) => ({ - name, - value: counts[name] || 0, - })); - - /* ---------- Summary Buckets ---------- */ - - const coordinationCompletedDeals = getDealsByResolvedStage( - deals, - ["In Design", "Completed"] - ); - - const designCompletedDeals = getDealsByResolvedStage( - deals, - ["Completed"] - ); - - const total = deals.length; - - /* ---------- Shared Summary Click Handler ---------- */ - const handleSummaryClick = ( - label: string, - stages: string[] -) => { - const filteredDeals = getDealsByResolvedStage( - deals, - stages - ); - - onOpenTable?.(label, filteredDeals); -}; - - - /* ---------- Stage Bar Click ---------- */ - - const handleBarClick = (value: { - name: string; - value: number; - }) => { - const filteredDeals = getDealsByResolvedStage( - deals, - [value.name] - ); - - onOpenTable?.(value.name, filteredDeals); - }; - - /* ---------- Split Normal vs Exception ---------- */ - - const normalStages = data.filter( - (d) => d.name !== "Queries" - ); - - const exceptionStages = data.filter( - (d) => d.name === "Queries" - ); - return ( -
- - {/* ===== Summary Panels ===== */} - - - - handleSummaryClick( - "Coordination Completed", - ["In Design", "Completed"] - ) - } - /> - - - handleSummaryClick( - "Design Completed", - ["Completed"] - ) - } - /> - - {/* ===== Main Progress Chart ===== */} - - -
- - Project Progress by Stage - -

- Click a bar to view related properties -

-

- Total: {total.toLocaleString()} properties -

-
- - -
- - {/* ===== Queries / Exception Chart ===== */} - - -
- - Needs HA Support & Not Viable - -

- Properties requiring attention -

-
- -
- -
-
- -
+ ); } \ No newline at end of file diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx index 9da2771e..76c2a9a4 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx @@ -3,6 +3,11 @@ interface ExpandableCountBarProps { title: string items: T[] + count?: number + percentage?: number + inProgressPercentage?: number + total?: number + secondaryStats?: Array<{ label: string; count: number }> onClick?: (items: T[]) => void className?: string } @@ -10,25 +15,106 @@ interface ExpandableCountBarProps { export default function ExpandableCountBar({ title, items, + count, + percentage, + inProgressPercentage, + total, + secondaryStats, onClick, className = "", }: ExpandableCountBarProps) { - const count = items.length + const displayCount = count ?? items.length + const displayTotal = total ?? items.length + const displayPercentage = percentage ?? 0 + const displayInProgressPercentage = inProgressPercentage ?? 0 + const radius = 40 + const circumference = 2 * Math.PI * radius + const completedStrokeDashoffset = circumference - (displayPercentage / 100) * circumference + const inProgressStrokeDashoffset = circumference - ((displayPercentage + displayInProgressPercentage) / 100) * circumference return ( -
onClick?.(items)} - className={`w-full cursor-pointer rounded-xl border bg-white shadow-sm hover:shadow-md transition-all duration-200 p-5 flex justify-between items-center ${className}`} + className={`w-full cursor-pointer rounded-xl border border-brandblue/20 bg-gradient-to-br from-brandlightblue/20 to-brandlightblue/5 shadow-sm hover:shadow-md hover:border-brandblue/40 transition-all duration-300 p-6 flex flex-col gap-4 group active:scale-95 ${className}`} > -
- {title} +
+
+

+ {title} +

+

Click to view breakdown

+
+ + {/* Circular Progress */} +
+ + {/* Background circle */} + + {/* In Progress circle (gold) - drawn first so it appears underneath */} + {displayInProgressPercentage > 0 && ( + + )} + {/* Completed progress circle (blue) - drawn last so it appears on top */} + {displayPercentage > 0 && ( + + )} + + {/* Center text */} +
+ + {displayPercentage.toFixed(0)}% + + + {displayCount}/{displayTotal} + +
+
-
- {count} items - -
-
+ {/* Secondary Stats */} + {secondaryStats && secondaryStats.length > 0 && ( +
+ {secondaryStats.map((stat) => ( +
+

{stat.label}

+

{stat.count}

+
+ ))} +
+ )} + + + ) } \ No newline at end of file diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx new file mode 100644 index 00000000..3abac38d --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -0,0 +1,347 @@ +"use client"; + +import { Card, Title } from "@tremor/react"; +import { AlertCircle } from "lucide-react"; +import { motion } from "framer-motion"; +import ExpandableCountBar from "./ExpandableCountBar"; + +interface ProgressOverviewProps { + deals: Record[]; + onOpenTable?: ( + stage: string, + deals: any[], + columns?: string[], + columnLabels?: Record, + breakdown?: Record + ) => void; +} + +export default function ProgressOverview({ deals, onOpenTable }: ProgressOverviewProps) { + const STAGE_ORDER = [ + "Scope & Planning", + "Booking in Progress", + "Assessment in Progress", + "Coordination in Progress", + "Design in Progress", + "Completed", + ]; + + const STAGE_LABELS: Record = { + "1617223910": "Scope & Planning", + "3583836399": "Scope & Planning", + "3589581001": "Booking in Progress", + "1984401629": "Assessment in Progress", + "2628233422": "AFTER_ASSESSMENT", + "2702650617": "AFTER_ASSESSMENT", + "2473886962": "AFTER_ASSESSMENT", + "1668803774": "AFTER_ASSESSMENT", + "1887735998": "Queries", + }; + + const getAfterAssessmentLabel = ( + coordinationStatus?: string, + designStatus?: string + ): string => { + const coordStatusUpper = coordinationStatus?.toUpperCase() ?? ""; + const designStatusUpper = designStatus?.toUpperCase() ?? ""; + + if (coordStatusUpper === "RA ISSUE") return "Queries"; + + if ( + coordStatusUpper.includes("(V1) IOE/MTP COMPLETE") || + coordStatusUpper.includes("(V2) IOE/MTP COMPLETE") || + coordStatusUpper.includes("(V3) IOE/MTP COMPLETE") + ) { + if (designStatusUpper === "UPLOADED") return "Completed"; + return "Design in Progress"; + } + + return "Coordination in Progress"; + }; + + const resolveDisplayStage = (deal: Record): string => { + let stageName = STAGE_LABELS[deal.dealstage] || "Unknown Stage"; + + if (stageName === "AFTER_ASSESSMENT") { + stageName = + getAfterAssessmentLabel( + deal.coordinationStatus, + deal.designStatus + ) || "Coordination in Progress"; + } + + if (stageName === "Scope & Planning") { + const coordStatusUpper = deal.coordinationStatus?.toUpperCase() ?? ""; + if (coordStatusUpper === "RA ISSUE") { + stageName = "Queries"; + } + } + + if (stageName === "Assessment in Progress") { + const coordStatusUpper = deal.coordinationStatus?.toUpperCase() ?? ""; + if (coordStatusUpper === "RA ISSUE") { + stageName = "Queries"; + } + } + + return stageName; + }; + + // Separate queries from main stages + const queriesDealsList = deals.filter((deal) => resolveDisplayStage(deal) === "Queries"); + + // Calculate stage distribution (excluding queries) + const stageCounts: Record = {}; + const stageDeals: Record = {}; + + deals.forEach((deal) => { + const stage = resolveDisplayStage(deal); + if (stage !== "Queries") { + stageCounts[stage] = (stageCounts[stage] || 0) + 1; + if (!stageDeals[stage]) stageDeals[stage] = []; + stageDeals[stage].push(deal); + } + }); + + const total = Object.values(stageCounts).reduce((a, b) => a + b, 0); + const completedCount = stageCounts["Completed"] || 0; + const completedPercentage = total > 0 ? (completedCount / total) * 100 : 0; + + // Calculate progress for each stage + const stageProgress = STAGE_ORDER.filter((s) => s !== "Queries").map((stage) => { + const count = stageCounts[stage] || 0; + const percentage = total > 0 ? (count / total) * 100 : 0; + return { stage, count, percentage, deals: stageDeals[stage] || [] }; + }); + + const handleStageClick = (item: any) => { + if (onOpenTable && item.deals.length > 0) { + onOpenTable(item.stage, item.deals, ["dealname", "landlordPropertyId"], { + dealname: "Address Ref.", + landlordPropertyId: "Property Ref.", + }); + } + }; + + const handleQueriesClick = () => { + if (onOpenTable && queriesDealsList.length > 0) { + onOpenTable( + "Properties Needing Attention", + queriesDealsList, + ["dealname", "landlordPropertyId", "coordinationStatus"], + { + dealname: "Address Ref.", + landlordPropertyId: "Property Ref.", + coordinationStatus: "Issue", + } + ); + } + }; + + const radius = 40 + const circumference = 2 * Math.PI * radius + const strokeDashoffset = circumference - (completedPercentage / 100) * circumference + + // Coordination and Design calculations + const completedDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Completed"); + const coordinationInProgressDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Coordination in Progress"); + const coordinationCompletedDeals = deals.filter((deal) => { + const stage = resolveDisplayStage(deal); + return stage === "Design in Progress" || stage === "Completed"; + }); + const designInProgressDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Design in Progress"); + const designCompletedDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Completed"); + + const totalDeals = deals.length; + const coordCompletedPercentage = totalDeals > 0 ? (coordinationCompletedDeals.length / totalDeals) * 100 : 0; + const coordInProgressPercentage = totalDeals > 0 ? (coordinationInProgressDeals.length / totalDeals) * 100 : 0; + const designCompletedPercentage = totalDeals > 0 ? (designCompletedDeals.length / totalDeals) * 100 : 0; + const designInProgressPercentageVal = totalDeals > 0 ? (designInProgressDeals.length / totalDeals) * 100 : 0; + + const coordinationBreakdown = { + "Coordination Completed": coordinationCompletedDeals, + "Coordination in Progress": coordinationInProgressDeals, + }; + + const handleSummaryClick = ( + label: string, + stages: string[], + breakdown?: Record + ) => { + const filteredDeals = deals.filter((deal) => { + const stage = resolveDisplayStage(deal); + return stages.includes(stage); + }); + onOpenTable?.(label, filteredDeals, undefined, undefined, breakdown); + }; + + return ( +
+ {/* Work Completed - Full Width Overview at Top */} + { + if (onOpenTable && completedDeals.length > 0) { + onOpenTable("Completed Properties", completedDeals, ["dealname", "landlordPropertyId"], { + dealname: "Address Ref.", + landlordPropertyId: "Property Ref.", + }); + } + }} + whileHover={{ scale: 1.02 }} + className="group relative text-left w-full" + > + +
+ {/* 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 + +
+
+
+
+ + {/* Project Summary Cards - Coordination & Design */} +
+ + handleSummaryClick( + "Coordination Status", + ["Design in Progress", "Completed", "Coordination in Progress"], + coordinationBreakdown + ) + } + /> + + + handleSummaryClick( + "Design Status", + ["Design in Progress", "Completed"] + ) + } + /> +
+ + {/* Queries / Attention Required Section */} + {queriesDealsList.length > 0 && ( + + +
+ {/* Header with Alert */} +
+
+ +
+
+

+ Requires Your Input +

+

+ These properties need your feedback or assistance to progress +

+
+
+ + {/* Count Display */} +
+

+ {queriesDealsList.length} +

+

+ {queriesDealsList.length === 1 ? "property" : "properties"} awaiting action +

+
+ + {/* CTA */} +
+ Review Details + +
+
+
+
+ )} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx index 31a7a6bb..e6c44489 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx @@ -4,12 +4,7 @@ import { useState } from "react"; import { DealStageChart } from "./DealStageChart"; import SurveyedPieChart from "./SurveyedResultsPieChart"; import TableViewer from "./TableViewer"; -import { - Card, - CardHeader, - CardTitle, - CardContent, -} from "@/app/shadcn_components/ui/card"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; import { Home, AlertTriangle } from "lucide-react"; import { motion } from "framer-motion"; @@ -35,6 +30,7 @@ export default function LiveTracker({ deals }: ReportsProps) { data: any[]; columns: string[]; columnLabels: Record; + breakdown?: Record; } | null>(null); const projectCodes = Object.keys(groupedDeals); @@ -67,7 +63,8 @@ export default function LiveTracker({ deals }: ReportsProps) { stage: string, filteredDeals: any[], columns?: string[], - columnLabels?: Record + columnLabels?: Record, + breakdown?: Record ) => { setOpenTable({ stage, @@ -79,23 +76,50 @@ export default function LiveTracker({ deals }: ReportsProps) { dealname: "Address Ref.", landlordPropertyId: "Property Ref.", }, + breakdown, }); }; if (!deals?.length) { return ( - + -

No deal data available.

+

No deal data available.

); } return ( -
+
{/* 🌍 Global Overview */}
+ {/* Project Selector */} + +
+

+ Select Project +

+
+ +
+ ▼ +
+
+
+
+ {/* Total Properties */} 0 ? "red" : "brandblue"} /> - - {/* Project Selector */} - - - -

- Select Project -

-
-
- -
- -
- ▼ -
-
-
-
{/* 📊 Project Insights */} - - - - Project-Level Insights — {currentProjectCode} - - +
+
+

+ Project-Level Insights — {currentProjectCode} +

+
- +
)} - - +
+
{/* 🔹 Table Modal */} {openTable && ( -
-
-

- {openTable.stage} — {openTable.data.length} Properties -

+
+ +
+

+ {openTable.stage} +

+

+ Showing {openTable.data.length} properties +

-
+ {/* Breakdown Stats */} + {openTable.breakdown && ( +
+ {Object.entries(openTable.breakdown).map(([category, items]) => ( +
+

+ {category} +

+

+ {items.length} +

+

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

+
+ ))} +
+ )} +
+ +
-
+
-
+
)}
@@ -254,30 +277,46 @@ function StatCard({ onClick: () => void; accent?: "brandblue" | "red"; }) { - const accentColor = - accent === "red" - ? "from-red-50 to-white text-red-600 hover:border-red-300" - : "from-brandlightblue/20 to-white text-brandblue hover:border-brandblue/40"; + 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-50/50 to-red-50/20", + border: "border-red-200/50", + text: "text-red-600", + value: "text-red-700", + hover: "hover:border-red-300 hover:shadow-lg", + icon: "text-red-600" + } + }; + + const config = accentConfig[accent]; return (
-

{title}

-

+

{title}

+

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

- +
); diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx index 8162454d..3ac61c30 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx @@ -66,13 +66,13 @@ export default function SurveyedPieChart({ } return ( - + {/* Header */} -
- + <div className="text-center mb-8 pb-6 border-b border-brandblue/10 w-full"> + <Title className="text-brandblue text-[16px] font-bold tracking-tight mb-2"> Survey Performance -

+

Click a segment or label to view filtered properties

@@ -94,14 +94,14 @@ export default function SurveyedPieChart({ const { name, amount } = item; return (
- + {name} - {amount.toLocaleString()} + {amount.toLocaleString()}
); @@ -109,7 +109,7 @@ export default function SurveyedPieChart({ /> {data.length > 0 && (
- + {data.reduce((a, b) => a + b.amount, 0)}
@@ -117,41 +117,41 @@ export default function SurveyedPieChart({
{/* Legend (Clean Grid Layout) */} -
+
{data.map((item, idx) => ( -
handleClick(item)} onMouseEnter={() => setHovered(item.name)} onMouseLeave={() => setHovered(null)} - className="relative flex items-center space-x-2 text-sm text-gray-700 hover:text-gray-900 cursor-pointer transition-colors" + className="relative flex items-center space-x-2 text-sm text-gray-700 hover:text-brandblue cursor-pointer transition-colors px-3 py-2 rounded-lg hover:bg-brandlightblue/20" > - + {item.name} - + {item.percentage}% {/* Tooltip on hover */} {hovered === item.name && (
- + {item.name} - {item.amount.toLocaleString()} + {item.amount.toLocaleString()}
)} -
+ ))}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx index f16b084b..1c7a3b37 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx @@ -78,10 +78,10 @@ export default function TableViewer({ ))}
@@ -92,21 +92,21 @@ export default function TableViewer({ }; return ( -
+
- + {visibleColumns.map((col) => ( @@ -133,10 +133,10 @@ export default function TableViewer({ filteredData.map((row, i) => ( {visibleColumns.map((col) => ( - ))} From bed89dbdec349a6d1a4975e08104d0acc164344a Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 26 Feb 2026 11:20:36 +0000 Subject: [PATCH 4/5] commit code that show complete guiness tracking --- .../your-projects/live/DealStageChart.tsx | 33 -- .../your-projects/live/ExpandableCountBar.tsx | 6 +- .../live/{Report.tsx => LiveTracker.tsx} | 259 +++++++------- .../your-projects/live/ProgressOverview.tsx | 315 +++++++----------- .../live/SurveyedResultsPieChart.tsx | 92 +++-- .../your-projects/live/TableViewer.tsx | 132 ++++++-- .../(portfolio)/your-projects/live/page.tsx | 9 +- .../your-projects/live/transforms.ts | 263 +++++++++++++++ .../(portfolio)/your-projects/live/types.ts | 153 +++++++++ 9 files changed, 830 insertions(+), 432 deletions(-) delete mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx rename src/app/portfolio/[slug]/(portfolio)/your-projects/live/{Report.tsx => LiveTracker.tsx} (53%) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx deleted file mode 100644 index 3bf3e0f8..00000000 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import ProgressOverview from "./ProgressOverview"; - -interface Deal { - dealname: string; - landlordPropertyId: string; - dealstage: string; - coordinationStatus?: string; - designStatus?: string; - [key: string]: any; -} - -interface DealStageChartProps { - deals: Deal[]; - onOpenTable?: ( - stageName: string, - filteredDeals: Deal[], - columns?: string[], - columnLabels?: Record, - breakdown?: Record - ) => void; -} - -export function DealStageChart({ - deals, - onOpenTable, -}: DealStageChartProps) { - - return ( - - ); -} \ No newline at end of file diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx index 76c2a9a4..d1bba6b5 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExpandableCountBar.tsx @@ -40,7 +40,7 @@ export default function ExpandableCountBar({ >
-

+

{title}

Click to view breakdown

@@ -105,10 +105,10 @@ export default function ExpandableCountBar({ {/* Secondary Stats */} {secondaryStats && secondaryStats.length > 0 && (
- {secondaryStats.map((stat) => ( + {secondaryStats.map((stat, index) => (

{stat.label}

-

{stat.count}

+

{stat.count}

))}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx similarity index 53% rename from src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index e6c44489..4e1770dd 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -1,86 +1,62 @@ "use client"; import { useState } from "react"; -import { DealStageChart } from "./DealStageChart"; -import SurveyedPieChart from "./SurveyedResultsPieChart"; +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"; -interface ReportsProps { - deals: Record[]; -} +export default function LiveTracker({ + projects, + totalDeals, + majorConditionDeals, +}: LiveTrackerProps) { + // UI State: which table modal is open + const [openTable, setOpenTable] = useState(null); -const MAJOR_CONDITION_STAGE_ID = "3061261536"; - -export default function LiveTracker({ deals }: ReportsProps) { - - const groupedDeals = deals.reduce( - (acc, deal) => { - const project = deal.projectCode || "Unknown Project"; - (acc[project] ||= []).push(deal); - return acc; - }, - {} as Record - ); - - const [openTable, setOpenTable] = useState<{ - stage: string; - data: any[]; - columns: string[]; - columnLabels: Record; - breakdown?: Record; - } | null>(null); - - const projectCodes = Object.keys(groupedDeals); + // UI State: which project tab is selected + const projectCodes = projects.map((p) => p.projectCode); const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]); - const currentDeals = groupedDeals[currentProjectCode]; + const currentProject = projects.find( + (p) => p.projectCode === currentProjectCode + ); - // Check if there's any survey data - const surveyorOutcomes = [ - "Surveyed", - "Surveyed - Pending Upload", - "Tenant Refusal", - "Other", - "Not Viable", - "Not Attempted", - "No Answer", - "Cancelled / No Show", - "Rescheduled", - ]; - const hasSurveyData = currentDeals.some((deal: any) => - deal.outcome && surveyorOutcomes.includes(deal.outcome) - ); - const totalProperties = deals.length; - const majorConditionDeals = deals.filter( - (d) => d.dealstage === MAJOR_CONDITION_STAGE_ID - ); + // Compute minor stuff inline (not data processing) const majorIssues = majorConditionDeals.length; - const majorPercent = ((majorIssues / totalProperties) * 100).toFixed(1); + const majorPercent = ((majorIssues / totalDeals) * 100).toFixed(1); + const hasSurveyData = (currentProject?.outcomePieSlices.length ?? 0) > 0; + + // 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); + } + } const handleOpenTable = ( stage: string, - filteredDeals: any[], - columns?: string[], - columnLabels?: Record, - breakdown?: Record + filteredDeals: ClassifiedDeal[], + columns?: (keyof HubspotDeal)[], + columnLabels?: Partial>, + breakdown?: Record ) => { setOpenTable({ stage, data: filteredDeals, - columns: - columns || ["dealname", "landlordPropertyId"], - columnLabels: - columnLabels || { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - }, + columns: columns || ["dealname", "landlordPropertyId"], + columnLabels: columnLabels || { + dealname: "Address Ref.", + landlordPropertyId: "Property Ref.", + }, breakdown, }); }; - if (!deals?.length) { + if (!totalDeals) { return ( @@ -113,9 +89,6 @@ export default function LiveTracker({ deals }: ReportsProps) { ))} -
- ▼ -
@@ -124,11 +97,11 @@ export default function LiveTracker({ deals }: ReportsProps) { handleOpenTable( "All Properties", - deals, + projects.flatMap((p) => p.allDeals), ["dealname", "landlordPropertyId", "projectCode"], { dealname: "Address Ref.", @@ -154,52 +127,62 @@ export default function LiveTracker({ deals }: ReportsProps) { "dealname", "landlordPropertyId", "majorConditionIssueDescription", - "majorConditionIssuePhotosS3" + "majorConditionIssuePhotosS3", ], { dealname: "Address Ref.", landlordPropertyId: "Property Ref.", majorConditionIssueDescription: "Surveyor's Notes", - majorConditionIssuePhotosS3: "Photo Evidence" + majorConditionIssuePhotosS3: "Photo Evidence", } ) } - accent={majorIssues > 0 ? "red" : "brandblue"} + accent={majorIssues > 0 ? "bright-red" : "red"} /> {/* 📊 Project Insights */} -
-
-

- Project-Level Insights — {currentProjectCode} -

-
+ {currentProject && ( +
+
+

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

+
-
- - - - - {hasSurveyData && ( - - )} + + {hasSurveyData && ( + + + + )} +
-
+ )} {/* 🔹 Table Modal */} {openTable && ( @@ -215,25 +198,54 @@ export default function LiveTracker({ deals }: ReportsProps) { {openTable.stage}

- Showing {openTable.data.length} properties + Showing{" "} + + {openTable.data.length} + {" "} + properties

{/* Breakdown Stats */} {openTable.breakdown && (
- {Object.entries(openTable.breakdown).map(([category, items]) => ( -
-

- {category} -

-

- {items.length} -

-

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

-
- ))} + {Object.entries(openTable.breakdown).map(([category, items]) => { + const isCompleted = category.includes("Completed"); + const bgColor = isCompleted + ? "bg-gradient-to-br from-brandblue/25 to-brandblue/15" + : "bg-gradient-to-br from-amber-100/40 to-amber-50/30"; + const borderColor = isCompleted + ? "border-brandblue/40" + : "border-amber-200/50"; + const textColor = isCompleted + ? "text-brandblue" + : "text-amber-600"; + const labelColor = isCompleted + ? "text-brandblue" + : "text-amber-600/70"; + + return ( +
+

+ {category} +

+

+ {items.length} +

+

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

+
+ ); + })}
)} @@ -243,6 +255,7 @@ export default function LiveTracker({ deals }: ReportsProps) { data={openTable.data} columns={openTable.columns} columnLabels={openTable.columnLabels} + breakdown={openTable.breakdown} /> @@ -275,7 +288,7 @@ function StatCard({ value: string | number; subtitle?: string; onClick: () => void; - accent?: "brandblue" | "red"; + accent?: "brandblue" | "red" | "bright-red"; }) { const accentConfig = { brandblue: { @@ -284,16 +297,24 @@ function StatCard({ text: "text-brandblue", value: "text-brandblue", hover: "hover:border-brandblue/40 hover:shadow-lg", - icon: "text-brandblue" + icon: "text-brandblue", }, red: { - gradient: "from-red-50/50 to-red-50/20", - border: "border-red-200/50", - text: "text-red-600", - value: "text-red-700", - hover: "hover:border-red-300 hover:shadow-lg", - icon: "text-red-600" - } + 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]; @@ -306,8 +327,14 @@ function StatCard({ >
-

{title}

-

+

+ {title} +

+

{value} {subtitle && ( @@ -316,7 +343,9 @@ function StatCard({ )}

- +
); 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 3abac38d..12a0bfd3 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -1,133 +1,100 @@ "use client"; -import { Card, Title } from "@tremor/react"; +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"; interface ProgressOverviewProps { - deals: Record[]; + data: ProjectProgressData; onOpenTable?: ( stage: string, - deals: any[], - columns?: string[], - columnLabels?: Record, - breakdown?: Record + deals: ClassifiedDeal[], + columns?: (keyof HubspotDeal)[], + columnLabels?: Partial>, + breakdown?: Record ) => void; } -export default function ProgressOverview({ deals, onOpenTable }: ProgressOverviewProps) { - const STAGE_ORDER = [ - "Scope & Planning", - "Booking in Progress", - "Assessment in Progress", - "Coordination in Progress", - "Design in Progress", - "Completed", - ]; +export default function ProgressOverview({ + data, + onOpenTable, +}: ProgressOverviewProps) { + // Pre-computed values from props + const { + completedPercentage, + completedCount, + totalDeals, + queriesDeals, + coordination, + design, + } = data; - const STAGE_LABELS: Record = { - "1617223910": "Scope & Planning", - "3583836399": "Scope & Planning", - "3589581001": "Booking in Progress", - "1984401629": "Assessment in Progress", - "2628233422": "AFTER_ASSESSMENT", - "2702650617": "AFTER_ASSESSMENT", - "2473886962": "AFTER_ASSESSMENT", - "1668803774": "AFTER_ASSESSMENT", - "1887735998": "Queries", + // 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 && data.completedDeals.length > 0) { + onOpenTable( + "Completed Properties", + data.completedDeals, + ["dealname", "landlordPropertyId"], + { + dealname: "Address Ref.", + landlordPropertyId: "Property Ref.", + } + ); + } }; - const getAfterAssessmentLabel = ( - coordinationStatus?: string, - designStatus?: string - ): string => { - const coordStatusUpper = coordinationStatus?.toUpperCase() ?? ""; - const designStatusUpper = designStatus?.toUpperCase() ?? ""; - - if (coordStatusUpper === "RA ISSUE") return "Queries"; - - if ( - coordStatusUpper.includes("(V1) IOE/MTP COMPLETE") || - coordStatusUpper.includes("(V2) IOE/MTP COMPLETE") || - coordStatusUpper.includes("(V3) IOE/MTP COMPLETE") - ) { - if (designStatusUpper === "UPLOADED") return "Completed"; - return "Design in Progress"; + 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 + ); } - - return "Coordination in Progress"; }; - const resolveDisplayStage = (deal: Record): string => { - let stageName = STAGE_LABELS[deal.dealstage] || "Unknown Stage"; - - if (stageName === "AFTER_ASSESSMENT") { - stageName = - getAfterAssessmentLabel( - deal.coordinationStatus, - deal.designStatus - ) || "Coordination in Progress"; - } - - if (stageName === "Scope & Planning") { - const coordStatusUpper = deal.coordinationStatus?.toUpperCase() ?? ""; - if (coordStatusUpper === "RA ISSUE") { - stageName = "Queries"; - } - } - - if (stageName === "Assessment in Progress") { - const coordStatusUpper = deal.coordinationStatus?.toUpperCase() ?? ""; - if (coordStatusUpper === "RA ISSUE") { - stageName = "Queries"; - } - } - - return stageName; - }; - - // Separate queries from main stages - const queriesDealsList = deals.filter((deal) => resolveDisplayStage(deal) === "Queries"); - - // Calculate stage distribution (excluding queries) - const stageCounts: Record = {}; - const stageDeals: Record = {}; - - deals.forEach((deal) => { - const stage = resolveDisplayStage(deal); - if (stage !== "Queries") { - stageCounts[stage] = (stageCounts[stage] || 0) + 1; - if (!stageDeals[stage]) stageDeals[stage] = []; - stageDeals[stage].push(deal); - } - }); - - const total = Object.values(stageCounts).reduce((a, b) => a + b, 0); - const completedCount = stageCounts["Completed"] || 0; - const completedPercentage = total > 0 ? (completedCount / total) * 100 : 0; - - // Calculate progress for each stage - const stageProgress = STAGE_ORDER.filter((s) => s !== "Queries").map((stage) => { - const count = stageCounts[stage] || 0; - const percentage = total > 0 ? (count / total) * 100 : 0; - return { stage, count, percentage, deals: stageDeals[stage] || [] }; - }); - - const handleStageClick = (item: any) => { - if (onOpenTable && item.deals.length > 0) { - onOpenTable(item.stage, item.deals, ["dealname", "landlordPropertyId"], { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - }); + 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 && queriesDealsList.length > 0) { + if (onOpenTable && queriesDeals.length > 0) { onOpenTable( "Properties Needing Attention", - queriesDealsList, + queriesDeals, ["dealname", "landlordPropertyId", "coordinationStatus"], { dealname: "Address Ref.", @@ -138,74 +105,33 @@ export default function ProgressOverview({ deals, onOpenTable }: ProgressOvervie } }; - const radius = 40 - const circumference = 2 * Math.PI * radius - const strokeDashoffset = circumference - (completedPercentage / 100) * circumference - - // Coordination and Design calculations - const completedDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Completed"); - const coordinationInProgressDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Coordination in Progress"); - const coordinationCompletedDeals = deals.filter((deal) => { - const stage = resolveDisplayStage(deal); - return stage === "Design in Progress" || stage === "Completed"; - }); - const designInProgressDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Design in Progress"); - const designCompletedDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Completed"); - - const totalDeals = deals.length; - const coordCompletedPercentage = totalDeals > 0 ? (coordinationCompletedDeals.length / totalDeals) * 100 : 0; - const coordInProgressPercentage = totalDeals > 0 ? (coordinationInProgressDeals.length / totalDeals) * 100 : 0; - const designCompletedPercentage = totalDeals > 0 ? (designCompletedDeals.length / totalDeals) * 100 : 0; - const designInProgressPercentageVal = totalDeals > 0 ? (designInProgressDeals.length / totalDeals) * 100 : 0; - - const coordinationBreakdown = { - "Coordination Completed": coordinationCompletedDeals, - "Coordination in Progress": coordinationInProgressDeals, - }; - - const handleSummaryClick = ( - label: string, - stages: string[], - breakdown?: Record - ) => { - const filteredDeals = deals.filter((deal) => { - const stage = resolveDisplayStage(deal); - return stages.includes(stage); - }); - onOpenTable?.(label, filteredDeals, undefined, undefined, breakdown); - }; - return (
{/* Work Completed - Full Width Overview at Top */} { - if (onOpenTable && completedDeals.length > 0) { - onOpenTable("Completed Properties", completedDeals, ["dealname", "landlordPropertyId"], { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - }); - } - }} + onClick={handleCompletedClick} whileHover={{ scale: 1.02 }} className="group relative text-left w-full" > - -
+ +
{/* Header with Circular Progress */} -
+
-

+

Work Completed

-

+

End-to-end project overview

{/* Circular Progress */} -
- +
+ {/* Background circle */} - + @@ -238,10 +170,10 @@ export default function ProgressOverview({ deals, onOpenTable }: ProgressOvervie {/* Center text */}
- + {completedPercentage.toFixed(0)}% - + {completedCount}/{totalDeals}
@@ -261,46 +193,44 @@ export default function ProgressOverview({ deals, onOpenTable }: ProgressOvervie
- handleSummaryClick( - "Coordination Status", - ["Design in Progress", "Completed", "Coordination in Progress"], - coordinationBreakdown - ) - } + items={[ + ...coordination.completedDeals, + ...coordination.inProgressDeals, + ]} + onClick={handleCoordinationClick} /> - handleSummaryClick( - "Design Status", - ["Design in Progress", "Completed"] - ) - } + items={[...design.completedDeals, ...design.inProgressDeals]} + onClick={handleDesignClick} />
{/* Queries / Attention Required Section */} - {queriesDealsList.length > 0 && ( + {queriesDeals.length > 0 && (

- {queriesDealsList.length} + {queriesDeals.length}

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

diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx index 3ac61c30..f1e46f38 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx @@ -1,29 +1,20 @@ "use client"; import { DonutChart, Card, Title } from "@tremor/react"; -import { useMemo, useState } from "react"; +import { useState } from "react"; +import type { OutcomeSlice, ClassifiedDeal } from "./types"; interface SurveyedPieChartProps { - deals: Record[]; - onOpenTable?: (outcome: string, filteredDeals: Record[]) => void; + slices: OutcomeSlice[]; + dealsByOutcome: Record; + onOpenTable?: (outcome: string, filteredDeals: ClassifiedDeal[]) => void; } -export default function SurveyedPieChart({ - deals, +export default function SurveyedResultsPieChart({ + slices, + dealsByOutcome, onOpenTable, }: SurveyedPieChartProps) { - const surveyorOutcomes = [ - "Surveyed", - "Surveyed - Pending Upload", - "Tenant Refusal", - "Other", - "Not Viable", - "Not Attempted", - "No Answer", - "Cancelled / No Show", - "Rescheduled", - ]; - const colors = [ "indigo-600", "indigo-400", @@ -36,35 +27,26 @@ export default function SurveyedPieChart({ "gray-200", ]; - const data = useMemo(() => { - const outcomeCounts: Record = {}; - deals.forEach((deal) => { - const outcome = deal.outcome; - if (outcome && surveyorOutcomes.includes(outcome)) { - outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1; - } - }); - const total = Object.values(outcomeCounts).reduce((a, b) => a + b, 0); - return Object.entries(outcomeCounts).map(([name, amount]) => ({ - name, - amount, - percentage: total ? ((amount / total) * 100).toFixed(1) : "0.0", - })); - }, [deals]); - - const handleClick = (value: { name: string; amount: number }) => { - if (!value) return; - const filteredDeals = deals.filter((d) => d.outcome === value.name); - onOpenTable?.(value.name, filteredDeals); - }; - const [hovered, setHovered] = useState(null); + const handleClick = (slice: OutcomeSlice) => { + if (!slice) return; + const filteredDeals = dealsByOutcome[slice.name] ?? []; + onOpenTable?.(slice.name, filteredDeals); + }; + // Don't show the chart if there's no data - if (data.length === 0) { + if (slices.length === 0) { return null; } + // Convert OutcomeSlice to chart data format + const chartData = slices.map((slice) => ({ + name: slice.name, + amount: slice.amount, + percentage: slice.percentage, + })); + return ( {/* Header */} @@ -80,7 +62,7 @@ export default function SurveyedPieChart({ {/* Donut Chart (Centered) */}
`${n.toLocaleString()}`} @@ -101,16 +83,18 @@ export default function SurveyedPieChart({ {name} - {amount.toLocaleString()} + + {amount.toLocaleString()} +
); }} /> - {data.length > 0 && ( + {slices.length > 0 && (
- {data.reduce((a, b) => a + b.amount, 0)} + {slices.reduce((a, b) => a + b.amount, 0)}
)} @@ -118,11 +102,11 @@ export default function SurveyedPieChart({ {/* Legend (Clean Grid Layout) */}
- {data.map((item, idx) => ( + {slices.map((slice, idx) => (
)} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx index 1c7a3b37..680c0bea 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx @@ -1,36 +1,95 @@ "use client"; -import { useState, useMemo } from "react"; +import { useState } from "react"; import { Download } from "lucide-react"; +import type { ClassifiedDeal, HubspotDeal } from "./types"; interface TableViewerProps { - data: Record[]; - columns?: string[]; - columnLabels?: Record; + data: ClassifiedDeal[]; + columns?: (keyof HubspotDeal)[]; + columnLabels?: Partial>; + breakdown?: Record; } export default function TableViewer({ data, columns, columnLabels, + breakdown, }: TableViewerProps) { const [searchTerms, setSearchTerms] = useState>({}); const visibleColumns = columns?.length ? columns - : Object.keys(data?.[0] || {}); + : (Object.keys(data?.[0] || {}) as (keyof HubspotDeal)[]); - const filteredData = useMemo(() => { - return data.filter((row) => - visibleColumns.every((col) => { - const term = searchTerms[col]?.toLowerCase() || ""; - if (!term) return true; - const value = String(row[col] ?? "").toLowerCase(); - return value.includes(term); - }) - ); - }, [data, searchTerms, visibleColumns]); + // Helper: Get category for a row based on breakdown + const getCategoryForRow = ( + row: ClassifiedDeal, + brk: Record | undefined + ): string | undefined => { + if (!brk) return undefined; + for (const [category, items] of Object.entries(brk)) { + if (items.includes(row)) return category; + } + return undefined; + }; - const renderCellContent = (col: string, value: any) => { + const getRowStatus = (row: ClassifiedDeal) => { + if (!breakdown) return "untouched"; + + const category = getCategoryForRow(row, breakdown); + if (category?.includes("Completed")) { + return "completed"; + } else if (category?.includes("Progress")) { + return "progress"; + } + return "untouched"; + }; + + const getRowBackgroundColor = (status: string) => { + switch (status) { + case "completed": + return "bg-white"; + case "progress": + return "bg-white"; + case "untouched": + return "bg-white"; + default: + return "bg-white"; + } + }; + + const getSortPriority = (status: string) => { + switch (status) { + case "completed": + return 0; + case "progress": + return 1; + case "untouched": + return 2; + default: + return 3; + } + }; + + // Inline filter derivation (no useMemo) + const filteredData = data.filter((row) => + visibleColumns.every((col) => { + const term = searchTerms[col]?.toLowerCase() || ""; + if (!term) return true; + const value = String(row[col as keyof ClassifiedDeal] ?? "").toLowerCase(); + return value.includes(term); + }) + ); + + // Inline sort derivation (no useMemo) + const sortedFilteredData = [...filteredData].sort((a, b) => { + const statusA = getRowStatus(a); + const statusB = getRowStatus(b); + return getSortPriority(statusA) - getSortPriority(statusB); + }); + + const renderCellContent = (col: keyof HubspotDeal, value: any) => { if (col === "majorConditionIssuePhotosS3" && value) { let urls: string[] = []; @@ -97,12 +156,11 @@ export default function TableViewer({
{visibleColumns.map((col) => ( - - {filteredData.length === 0 ? ( + {sortedFilteredData.length === 0 ? ( ) : ( - filteredData.map((row, i) => ( - - {visibleColumns.map((col) => ( - - ))} - - )) + sortedFilteredData.map((row, i) => { + const status = getRowStatus(row); + return ( + + {visibleColumns.map((col) => ( + + ))} + + ); + }) )}
-
- {columnLabels?.[col] || col} +
+ {columnLabels?.[col] || col} setSearchTerms((prev) => ({ ...prev, @@ -124,7 +124,7 @@ export default function TableViewer({
No results found
+ {renderCellContent(col, row[col])}
+
- {columnLabels?.[col] || col} + + {columnLabels?.[col] || (col as string)} +
- {renderCellContent(col, row[col])} -
+ {renderCellContent( + col, + row[col as keyof ClassifiedDeal] + )} +
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 35542966..53b04a45 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -5,7 +5,9 @@ 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 "./Report"; +import LiveTracker from "./LiveTracker"; +import { computeLiveTrackerData } from "./transforms"; +import type { HubspotDeal } from "./types"; export default async function LiveReportingPage(props: { params: Promise<{ slug: string }>; @@ -51,6 +53,9 @@ export default async function LiveReportingPage(props: { ); } + // 🔄 Transform raw deals to typed and computed data + const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); + return (
@@ -63,7 +68,7 @@ export default async function LiveReportingPage(props: {
- +
); } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts new file mode 100644 index 00000000..5d7be344 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -0,0 +1,263 @@ +/** + * Live Tracking Feature - Pure Data Transformation Functions + * No React, no hooks, no side effects. All business logic lives here. + */ + +import type { + HubspotDeal, + ClassifiedDeal, + DisplayStage, + ProjectProgressData, + ProjectData, + OutcomeSlice, + LiveTrackerProps, + WorkPhaseStats, +} from "./types"; + +import { + STAGE_ORDER, + SURVEYOR_OUTCOMES, + MAJOR_CONDITION_STAGE_ID, +} from "./types"; + +// ----------------------------------------------------------------------- +// Stage ID -> raw label mapping +// ----------------------------------------------------------------------- +const STAGE_ID_MAP: Record = { + "1617223910": "Scope & Planning", + "3583836399": "Scope & Planning", + "3589581001": "Booking in Progress", + "1984401629": "Assessment in Progress", + "2628233422": "AFTER_ASSESSMENT", + "2702650617": "AFTER_ASSESSMENT", + "2473886962": "AFTER_ASSESSMENT", + "1668803774": "AFTER_ASSESSMENT", + "1887735998": "Queries", +}; + +// ----------------------------------------------------------------------- +// After-assessment sub-classification +// Resolves AFTER_ASSESSMENT deals based on coordinationStatus and designStatus +// ----------------------------------------------------------------------- +function resolveAfterAssessmentStage( + coordinationStatus: string | null, + designStatus: string | null +): DisplayStage { + const coord = coordinationStatus?.toUpperCase() ?? ""; + const design = designStatus?.toUpperCase() ?? ""; + + // RA ISSUE always -> Queries + if (coord === "RA ISSUE") return "Queries"; + + // V1/V2/V3 IOE/MTP COMPLETE pattern + if ( + coord.includes("(V1) IOE/MTP COMPLETE") || + coord.includes("(V2) IOE/MTP COMPLETE") || + coord.includes("(V3) IOE/MTP COMPLETE") + ) { + return design === "UPLOADED" ? "Completed" : "Design in Progress"; + } + + // Default for AFTER_ASSESSMENT + return "Coordination in Progress"; +} + +// ----------------------------------------------------------------------- +// Resolve display stage for a single deal +// Maps dealstage ID + coordinationStatus + designStatus -> DisplayStage +// ----------------------------------------------------------------------- +export function resolveDisplayStage(deal: HubspotDeal): DisplayStage { + const raw = STAGE_ID_MAP[deal.dealstage ?? ""] ?? "Unknown Stage"; + + if (raw === "AFTER_ASSESSMENT") { + return resolveAfterAssessmentStage( + deal.coordinationStatus, + deal.designStatus + ); + } + + // RA ISSUE override can apply to other stages too + if (raw === "Scope & Planning" || raw === "Assessment in Progress") { + if (deal.coordinationStatus?.toUpperCase() === "RA ISSUE") { + return "Queries"; + } + } + + return raw as DisplayStage; +} + +// ----------------------------------------------------------------------- +// Classify all deals in a list +// Adds displayStage to each deal +// ----------------------------------------------------------------------- +export function classifyDeals(deals: HubspotDeal[]): ClassifiedDeal[] { + return deals.map((deal) => ({ + ...deal, + displayStage: resolveDisplayStage(deal), + })); +} + +// ----------------------------------------------------------------------- +// Compute all ProjectProgressData for a set of already-classified deals +// ----------------------------------------------------------------------- +export function computeProjectProgress( + deals: ClassifiedDeal[] +): ProjectProgressData { + const queriesDeals = deals.filter((d) => d.displayStage === "Queries"); + const nonQueryDeals = deals.filter((d) => d.displayStage !== "Queries"); + const nonQueryTotal = nonQueryDeals.length; + + // Stage counts/percentages (queries excluded from percentage calculation) + const stageBuckets: Record = {}; + for (const deal of nonQueryDeals) { + (stageBuckets[deal.displayStage] ??= []).push(deal); + } + + const stageProgress = STAGE_ORDER.filter((s) => s !== "Queries").map( + (stage) => { + const stageDeals = stageBuckets[stage] ?? []; + return { + stage, + count: stageDeals.length, + percentage: + nonQueryTotal > 0 ? (stageDeals.length / nonQueryTotal) * 100 : 0, + deals: stageDeals, + }; + } + ); + + const completedDeals = stageBuckets["Completed"] ?? []; + const completedCount = completedDeals.length; + const completedPercentage = + nonQueryTotal > 0 ? (completedCount / nonQueryTotal) * 100 : 0; + + const totalDeals = deals.length; + + // Coordination phase: + // completed = Design in Progress + Completed (i.e. coordination is done) + // in progress = Coordination in Progress + const coordCompletedDeals = deals.filter( + (d) => + d.displayStage === "Design in Progress" || + d.displayStage === "Completed" + ); + const coordInProgressDeals = deals.filter( + (d) => d.displayStage === "Coordination in Progress" + ); + + const coordination: WorkPhaseStats = { + completedDeals: coordCompletedDeals, + inProgressDeals: coordInProgressDeals, + completedCount: coordCompletedDeals.length, + inProgressCount: coordInProgressDeals.length, + completedPercentage: + totalDeals > 0 + ? (coordCompletedDeals.length / totalDeals) * 100 + : 0, + inProgressPercentage: + totalDeals > 0 + ? (coordInProgressDeals.length / totalDeals) * 100 + : 0, + total: totalDeals, + }; + + // Design phase: + // completed = Completed stage + // in progress = Design in Progress + const designInProgressDeals = deals.filter( + (d) => d.displayStage === "Design in Progress" + ); + + const design: WorkPhaseStats = { + completedDeals, + inProgressDeals: designInProgressDeals, + completedCount, + inProgressCount: designInProgressDeals.length, + completedPercentage: + totalDeals > 0 ? (completedCount / totalDeals) * 100 : 0, + inProgressPercentage: + totalDeals > 0 + ? (designInProgressDeals.length / totalDeals) * 100 + : 0, + total: totalDeals, + }; + + return { + stageProgress, + queriesDeals, + completedDeals, + completedCount, + completedPercentage, + nonQueryTotal, + totalDeals, + coordination, + design, + }; +} + +// ----------------------------------------------------------------------- +// Compute outcome pie slices for the surveyed pie chart +// ----------------------------------------------------------------------- +export function computeOutcomeSlices(deals: ClassifiedDeal[]): OutcomeSlice[] { + const counts: Partial> = {}; + + for (const deal of deals) { + if ( + deal.outcome && + (SURVEYOR_OUTCOMES as readonly string[]).includes(deal.outcome) + ) { + counts[deal.outcome] = (counts[deal.outcome] ?? 0) + 1; + } + } + + const total = Object.values(counts).reduce( + (a, b) => a + (b ?? 0), + 0 + ); + + return Object.entries(counts).map(([name, amount]) => ({ + name, + amount: amount ?? 0, + percentage: + total > 0 ? (((amount ?? 0) / total) * 100).toFixed(1) : "0.0", + })); +} + +// ----------------------------------------------------------------------- +// Top-level function called by page.tsx +// Orchestrates all transformations: classify, group by project, compute stats +// ----------------------------------------------------------------------- +export function computeLiveTrackerData( + rawDeals: HubspotDeal[] +): LiveTrackerProps { + // Classify all deals (add displayStage field) + const classified = classifyDeals(rawDeals); + + // Filter for major condition deals (Awaab's Law) + const majorConditionDeals = classified.filter( + (d) => d.dealstage === MAJOR_CONDITION_STAGE_ID + ); + + // Group deals by projectCode + const grouped: Record = {}; + for (const deal of classified) { + const key = deal.projectCode ?? "Unknown Project"; + (grouped[key] ??= []).push(deal); + } + + // For each project group, compute progress data and outcome slices + const projects: ProjectData[] = Object.entries(grouped).map( + ([projectCode, deals]) => ({ + projectCode, + progress: computeProjectProgress(deals), + outcomePieSlices: computeOutcomeSlices(deals), + allDeals: deals, + }) + ); + + return { + projects, + totalDeals: classified.length, + majorConditionDeals, + }; +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts new file mode 100644 index 00000000..229a2ede --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -0,0 +1,153 @@ +/** + * Live Tracking Feature - Type Definitions + * Single source of truth for all TypeScript interfaces and constants + */ + +// ----------------------------------------------------------------------- +// Raw DB row from hubspotDealData table +// ----------------------------------------------------------------------- +export type HubspotDeal = { + id: string; + dealId: string; + dealname: string | null; + dealstage: string | null; + companyId: string | null; + projectCode: string | null; + landlordPropertyId: string | null; + uprn: string | null; + outcome: string | null; + outcomeNotes: string | null; + majorConditionIssueDescription: string | null; + majorConditionIssuePhotos: string | null; + majorConditionIssuePhotosS3: string | null; + coordinationStatus: string | null; + designStatus: string | null; + createdAt: Date; + updatedAt: Date; +}; + +// ----------------------------------------------------------------------- +// Stage classification result - human-readable display labels +// ----------------------------------------------------------------------- +export type DisplayStage = + | "Scope & Planning" + | "Booking in Progress" + | "Assessment in Progress" + | "Coordination in Progress" + | "Design in Progress" + | "Completed" + | "Queries" + | "Unknown Stage"; + +// ----------------------------------------------------------------------- +// A classified deal - original row plus its resolved display stage +// ----------------------------------------------------------------------- +export type ClassifiedDeal = HubspotDeal & { + displayStage: DisplayStage; +}; + +// ----------------------------------------------------------------------- +// One entry in the stage progress bar list +// ----------------------------------------------------------------------- +export type StageProgressItem = { + stage: DisplayStage; + count: number; + percentage: number; // out of non-query total + deals: ClassifiedDeal[]; +}; + +// ----------------------------------------------------------------------- +// Coordination/Design summary card data +// ----------------------------------------------------------------------- +export type WorkPhaseStats = { + completedDeals: ClassifiedDeal[]; + inProgressDeals: ClassifiedDeal[]; + completedCount: number; + inProgressCount: number; + completedPercentage: number; // out of ALL deals in project + inProgressPercentage: number; + total: number; +}; + +// ----------------------------------------------------------------------- +// All computed data for the ProgressOverview component +// ----------------------------------------------------------------------- +export type ProjectProgressData = { + stageProgress: StageProgressItem[]; + queriesDeals: ClassifiedDeal[]; + completedDeals: ClassifiedDeal[]; + completedCount: number; + completedPercentage: number; // out of non-query total + nonQueryTotal: number; + totalDeals: number; + coordination: WorkPhaseStats; + design: WorkPhaseStats; +}; + +// ----------------------------------------------------------------------- +// Surveyed outcome entry (for pie chart) +// ----------------------------------------------------------------------- +export type OutcomeSlice = { + name: string; // outcome label + amount: number; + percentage: string; // pre-formatted "12.3" +}; + +// ----------------------------------------------------------------------- +// What LiveTracker receives from page.tsx for one project +// ----------------------------------------------------------------------- +export type ProjectData = { + projectCode: string; + progress: ProjectProgressData; + outcomePieSlices: OutcomeSlice[]; // empty array = hide pie chart + allDeals: ClassifiedDeal[]; // for table drill-downs within project +}; + +// ----------------------------------------------------------------------- +// Top-level props for LiveTracker (client root) +// ----------------------------------------------------------------------- +export type LiveTrackerProps = { + projects: ProjectData[]; + totalDeals: number; + majorConditionDeals: ClassifiedDeal[]; // for Awaab's Law card +}; + +// ----------------------------------------------------------------------- +// Table drill-down shape (stays in LiveTracker state) +// ----------------------------------------------------------------------- +export type TableModal = { + stage: string; + data: ClassifiedDeal[]; + columns: (keyof HubspotDeal)[]; + columnLabels: Partial>; + breakdown?: Record; +}; + +// ----------------------------------------------------------------------- +// Surveyor outcome constants (single source of truth) +// ----------------------------------------------------------------------- +export const SURVEYOR_OUTCOMES = [ + "Surveyed", + "Surveyed - Pending Upload", + "Tenant Refusal", + "Other", + "Not Viable", + "Not Attempted", + "No Answer", + "Cancelled / No Show", + "Rescheduled", +] as const; + +export type SurveyorOutcome = (typeof SURVEYOR_OUTCOMES)[number]; + +export const MAJOR_CONDITION_STAGE_ID = "3061261536" as const; + +// Order of stages for grouping/display (queries excluded from this list) +export const STAGE_ORDER: DisplayStage[] = [ + "Scope & Planning", + "Booking in Progress", + "Assessment in Progress", + "Coordination in Progress", + "Design in Progress", + "Completed", +]; From 24fa2c44d2023f2cc3ba0b03f85307e4fbe8dca4 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 26 Feb 2026 14:23:05 +0000 Subject: [PATCH 5/5] fixing some minor bugs --- .../your-projects/live/LiveTracker.tsx | 11 +++- .../your-projects/live/ProgressOverview.tsx | 63 ++++++++++++++++++- 2 files changed, 71 insertions(+), 3 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 4e1770dd..10017d77 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -186,12 +186,16 @@ export default function LiveTracker({ {/* 🔹 Table Modal */} {openTable && ( -
+
setOpenTable(null)} + > e.stopPropagation()} >

@@ -261,7 +265,10 @@ export default function LiveTracker({