Merge pull request #179 from Hestia-Homes/feature/guiness_live_tracking

Feature/guiness live tracking
This commit is contained in:
Jun-te Kim 2026-02-20 14:55:55 +00:00 committed by GitHub
commit 140fd27263
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 143 additions and 43 deletions

View file

@ -19,7 +19,8 @@
"editor.insertSpaces": true
},
"extensions": [
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"Anthropic.claude-code"
]
}
}

View file

@ -19,6 +19,9 @@ export const hubspotDealData = pgTable("hubspot_deal_data", {
majorConditionIssuePhotos: text("major_condition_issue_photos"),
majorConditionIssuePhotosS3: text("major_condition_issue_evidence_s3_url"),
coordinationStatus: text("coordination_status"),
designStatus: text("design_status"),
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
.defaultNow()
.notNull(),

View file

@ -5,17 +5,48 @@ import { BarList, Card, Title } from "@tremor/react";
const STAGE_ORDER = [
"Initial planning",
"Booking team to contact tenant",
"Survey in progress",
"Coordination + design",
"Ready for install",
"Installed - Ready for Post work EPC",
"Needs support from HA",
"Not viable for funding",
"Booking Team to contact tenant",
"In Assessment",
"In Coordination",
"In Design",
"Completed",
"Queries",
];
const stage = (label: string) => STAGE_ORDER.find((s) => s === label)!;
// 🔧 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";
}
// 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'
return "In Design";
}
// Default to 'In Coordination'
return "In Coordination";
};
// 🏷️ Deal stage → display stage mapping
const STAGE_LABELS: Record<string, string> = {
"1617223910": stage("Initial planning"), // 0 - [Ops] Backlog
@ -27,34 +58,40 @@ const STAGE_LABELS: Record<string, string> = {
"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("Needs support from HA"), // 4 - [Bookings/Sales] Booking issues - needs HA support (Check with Aidan)
"1984401629": stage("Survey in progress"), // 2 - [Bookings/Ops/Sales] No Contact Details - Ready for Route
"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
"1617223912": stage("Survey in progress"), // 2 - [Ops] Ready for Assignment to Route
"1617223913": stage("Survey in progress"), // 2 - [Ops] Survey in Progress
"3206388924": stage("Survey in progress"), // 2 - [Ops] Surveyed - Pending Upload from Surveyor
"1617223915": stage("Survey in progress"), // 2 - [Ops] No Access - Need Sign Off
"1617223917": stage("Not viable for funding"), // 3 - [Ops] No Access - No Revisit
"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("Coordination + design"), // 5 - [Ops] Properties to Review Manually
"2628341989": stage("Coordination + design"), // 5 - [Ops] Assessment needs correction
"3441170637": stage("Coordination + design"), // 5 - [Ops] Awaiting PV Design
"1617223916": stage("In Assessment"), // 5 - [Ops] Properties to Review Manually
"1887735998": stage("Not viable for funding"), // 3 - [Ops] Not Viable
"3061261536": stage("Needs support from HA"), // 4 - [Sales/Tech] Major condition issue
"1887735999": stage("Needs support from HA"), // 4 - [Ops] Needs HA Works
"3016601828": stage("Needs support from HA"), // 4 - [Engagement Team] EPC C Before Works
"1617223914": stage("Coordination + design"), // 5 - [Ops] Surveyed in Pashub, Transit Job to Co-ordination
"2628233422": stage("Coordination + design"), // 5 - [Coordination] Ready for coordination
"2702650617": stage("Coordination + design"), // 5 - [Design] Ready for Design
"2473886962": stage("Coordination + design"), // 5 - [Design] Design in progress
// 🔧 ===== 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
"1668803774": stage("Ready for install"), // 6 - [Finance] Ready for Invoicing
"3440363736": stage("Ready for install"), // 6 - [Finance] Needs Invoicing - Files Sent
"2769407183": stage("Needs support from HA"), // 4 - [Ops] PV - Needs Heating Upgrade (Pre EPR D)
"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)
};
// 🧩 Reasons for exception stages (HA support / Not viable)
@ -76,6 +113,8 @@ interface Deal {
dealname: string;
landlordPropertyId: string;
dealstage: string;
coordinationStatus?: string;
designStatus?: string;
reason?: string;
[key: string]: any;
}
@ -96,7 +135,22 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
deals.forEach((d) => {
const stageId = d.dealstage || "unknown";
const stageName = STAGE_LABELS[stageId] || "Unknown Stage";
let stageName = STAGE_LABELS[stageId] || "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
}
// 🔧 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";
}
}
counts[stageName] = (counts[stageName] || 0) + 1;
});
@ -111,7 +165,22 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
const handleBarClick = (value: { name: string; value: number }) => {
const filteredDeals: Deal[] = deals
.filter((d) => {
const stageName = STAGE_LABELS[d.dealstage] || "Unknown Stage";
let stageName = STAGE_LABELS[d.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
}
// 🔧 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) => ({
@ -147,12 +216,12 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
// Split into normal + exception stages
const normalStages = data.filter(
(d) =>
!["Needs support from HA", "Not viable for funding"].includes(d.name) &&
!["Queries"].includes(d.name) &&
d.name !== ""
);
const exceptionStages = data.filter((d) =>
["Needs support from HA", "Not viable for funding"].includes(d.name)
["Queries"].includes(d.name)
);
return (

View file

@ -16,9 +16,11 @@ import { motion } from "framer-motion";
interface ReportsProps {
deals: Record<string, any>[];
}
const MAJOR_CONDITION_STAGE_ID = "3061261536";
export default function LiveTracker({ deals }: ReportsProps) {
const groupedDeals = deals.reduce(
(acc, deal) => {
const project = deal.projectCode || "Unknown Project";
@ -38,6 +40,22 @@ export default function LiveTracker({ deals }: ReportsProps) {
const projectCodes = Object.keys(groupedDeals);
const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]);
const currentDeals = groupedDeals[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
@ -164,7 +182,7 @@ export default function LiveTracker({ deals }: ReportsProps) {
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
<CardContent className={`grid gap-6 ${hasSurveyData ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1 max-w-2xl mx-auto"}`}>
<motion.div
whileHover={{ scale: 1.01 }}
className="border rounded-xl p-5 bg-white shadow-sm hover:shadow-md transition"
@ -175,15 +193,17 @@ export default function LiveTracker({ deals }: ReportsProps) {
/>
</motion.div>
<motion.div
whileHover={{ scale: 1.01 }}
className="border rounded-xl p-5 bg-white shadow-sm hover:shadow-md transition"
>
<SurveyedPieChart
deals={currentDeals}
onOpenTable={handleOpenTable}
/>
</motion.div>
{hasSurveyData && (
<motion.div
whileHover={{ scale: 1.01 }}
className="border rounded-xl p-5 bg-white shadow-sm hover:shadow-md transition"
>
<SurveyedPieChart
deals={currentDeals}
onOpenTable={handleOpenTable}
/>
</motion.div>
)}
</CardContent>
</Card>

View file

@ -60,6 +60,11 @@ export default function SurveyedPieChart({
const [hovered, setHovered] = useState<string | null>(null);
// Don't show the chart if there's no data
if (data.length === 0) {
return null;
}
return (
<Card className="flex flex-col items-center p-6 pt-10 pb-8 bg-white">
{/* Header */}

View file

@ -67,3 +67,5 @@ export default async function LiveReportingPage(props: {
</div>
);
}