save working version of summary information

This commit is contained in:
Jun-te Kim 2026-02-25 12:32:21 +00:00
parent 302bdaf366
commit 9923e5ee13

View file

@ -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<string, string> = {
"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<string, string> = {
// ---- Needs support from HA ----
"2663668937": "Booking issues due to tenant difficulties.",
"3061261536": "Awaab's Law",
"1887735999": "<Please contact the Tech Team for implementation>",
"3016601828": "RA is currently EPR C. Convert to EPC?",
"2769407183": "Needs HA heating upgrade. Domna/HA discussion required.",
/* ================================
TYPES
================================ */
// ---- Not viable for funding ----
"1617223917": "<Please contact the Tech Team for implementation>",
"1887735998": "<Please contact the Tech Team for implementation>",
};
// ✅ 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<string, number> = {};
/* ================================
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<string, number> = {};
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<string, string>);
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 (
<div className="flex flex-col gap-3">
{/* ✅ Main Progress Chart */}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6 flex flex-col items-center justify-center">
<div className="text-center mb-3">
<Title className="text-gray-800 text-base font-semibold">
Project Progress by Stage
</Title>
<p className="text-xs text-gray-500 mt-0.5">
Click a bar to view related properties
</p>
<p className="text-xs text-gray-700 font-medium mt-1">
Total: {total.toLocaleString()} properties
</p>
</div>
return (
<div className="flex flex-col gap-4">
{/* ===== Summary Panels ===== */}
<ExpandableCountBar
title="Coordination Completed"
items={coordinationCompletedDeals}
onClick={() =>
handleSummaryClick(
"Coordination Completed",
["In Design", "Completed"]
)
}
/>
<ExpandableCountBar
title="Design Completed"
items={designCompletedDeals}
onClick={() =>
handleSummaryClick(
"Design Completed",
["Completed"]
)
}
/>
{/* ===== Main Progress Chart ===== */}
<Card className="bg-white rounded-xl shadow-sm p-6">
<div className="text-center mb-3">
<Title className="text-base font-semibold">
Project Progress by Stage
</Title>
<p className="text-xs text-gray-500">
Click a bar to view related properties
</p>
<p className="text-xs font-medium mt-1">
Total: {total.toLocaleString()} properties
</p>
</div>
<div className="w-full max-w-md">
<BarList
data={normalStages}
color="blue"
@ -248,30 +248,31 @@ return (
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</div>
</Card>
</Card>
{/* 🚨 Exception Chart */}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6 flex flex-col items-center justify-center">
<div className="text-center mb-3">
<Title className="text-gray-800 text-base font-semibold">
Needs HA Support & Not Viable
</Title>
<p className="text-xs text-gray-500 mt-0.5">
Click to explore exception properties (reasons appear in table)
</p>
</div>
{/* ===== Queries / Exception Chart ===== */}
<div className="w-full max-w-md">
<BarList
data={exceptionStages}
color="red"
sortOrder="none"
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</div>
</Card>
</div>
);
}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6 flex flex-col items-center justify-center">
<div className="text-center mb-3">
<Title className="text-gray-800 text-base font-semibold">
Needs HA Support & Not Viable
</Title>
<p className="text-xs text-gray-500 mt-0.5">
Properties requiring attention
</p>
</div>
<div className="w-full max-w-md">
<BarList
data={exceptionStages}
color="red"
sortOrder="none"
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</div>
</Card>
</div>
);
}