From 694871a36b4c12c0bd68a9d9be0cc40e5d8869b5 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 25 Feb 2026 14:53:51 +0000 Subject: [PATCH] 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 3851c52..3bf3e0f 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 9da2771..76c2a9a 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 0000000..3abac38 --- /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 31a7a6b..e6c4448 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 8162454..3ac61c3 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 f16b084..1c7a3b3 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) => ( - ))}
-
- {columnLabels?.[col] || col} +
+ {columnLabels?.[col] || col} setSearchTerms((prev) => ({ ...prev, @@ -124,7 +124,7 @@ export default function TableViewer({
No results found
+ {renderCellContent(col, row[col])}