push to production

This commit is contained in:
Jun-te Kim 2025-11-06 12:44:42 +00:00
parent ece75781b9
commit b3253d7e84
3 changed files with 224 additions and 122 deletions

View file

@ -5,7 +5,6 @@ import MicrosoftSignInButton from "./components/signin/MicrosoftSignInButton";
import EmailSignInButton from "./components/signin/EmailSignInButton";
import { redirect } from "next/navigation";
import Image from "next/image";
import "@tremor/react/dist/esm/tremor.css";
export default async function Home(props: {

View file

@ -9,56 +9,85 @@ const STAGE_ORDER = [
"Survey in progress",
"Coordination + design",
"Ready for install",
"Installed",
"Installed - Ready for Post work EPC",
"Needs support from HA",
"Not viable for funding",
];
const stage = (label: string) => STAGE_ORDER.find((s) => s === label)!;
// 🏷️ Deal stage → display stage mapping
const STAGE_LABELS: Record<string, string> = {
"1617223910": stage("Initial planning"),
"3583836399": stage("Initial planning"),
"1617223910": stage("Initial planning"), // 0 - [Ops] Backlog
"3583836399": stage("Initial planning"), // 0 - [Ops] Route Planning
"3589581001": stage("Booking team to contact tenant"),
"3569878239": stage("Booking team to contact tenant"),
"1617223911": stage("Booking team to contact tenant"),
"1984184569": stage("Booking team to contact tenant"),
"3569572028": stage("Booking team to contact tenant"),
"3570936026": stage("Booking team to contact tenant"),
"2663668937": stage("Needs support from HA"),
"1984401629": stage("Survey in progress"),
"2558220518": stage("Booking team to contact tenant"),
"3474594026": stage("Booking team to contact tenant"),
"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("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
"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"),
"1617223913": stage("Survey in progress"),
"3206388924": stage("Survey in progress"),
"1617223915": stage("Survey in progress"),
"1617223917": stage("Not viable for funding"),
"2571417798": stage("Booking team to contact tenant"),
"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
"2571417798": stage("Booking team to contact tenant"), // 1 - [Ops] Surveyed under 2019 - Needs Re-survey
"1617223916": stage("Coordination + design"),
"2628341989": stage("Coordination + design"),
"3441170637": stage("Coordination + design"),
"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
"1887735998": stage("Not viable for funding"),
"3061261536": stage("Needs support from HA"),
"1887735999": stage("Needs support from HA"),
"3016601828": stage("Needs support from HA"),
"1617223914": stage("Coordination + design"),
"2628233422": stage("Coordination + design"),
"2702650617": stage("Coordination + design"),
"2473886962": stage("Coordination + design"),
"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
"1668803774": stage("Ready for install"),
"3440363736": stage("Ready for install"),
"2769407183": stage("Needs support from HA"),
"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)
};
// 🧩 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.",
// ---- 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;
reason?: string;
[key: string]: any;
}
interface DealStageChartProps {
deals: any[];
onOpenTable?: (stageName: string, filteredDeals: any[]) => void;
deals: Deal[];
onOpenTable?: (
stageName: string,
filteredDeals: Deal[],
columns?: string[],
columnLabels?: { [key: string]: string }
) => void;
}
export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
@ -77,44 +106,72 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
}));
}, [deals]);
const total = useMemo(() => deals.length, [deals]);
const total = deals.length;
const handleBarClick = (value: { name: string; value: number }) => {
const filtered = deals.filter((d) => {
const stageId = d.dealstage || "unknown";
const stageName = STAGE_LABELS[stageId] || "Unknown Stage";
return stageName === value.name;
});
onOpenTable?.(value.name, filtered);
const filteredDeals: Deal[] = deals
.filter((d) => {
const stageName = STAGE_LABELS[d.dealstage] || "Unknown Stage";
return stageName === value.name;
})
.map((d) => ({
...d,
// ✅ Always provide a string to avoid undefined issues
reason: STAGE_REASONS[d.dealstage] ?? "",
}));
const isException =
value.name === "Needs support from HA" ||
value.name === "Not viable for funding";
// 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>);
};
// ✅ Split into normal and exception stages
// Split into normal + exception stages
const normalStages = data.filter(
(d) =>
!["Needs support from HA", "Not viable for funding"].includes(d.name) &&
d.name &&
d.name !== ""
);
const exceptionStages = data.filter((d) =>
["Needs support from HA", "Not viable for funding"].includes(d.name)
);
return (
<div className="flex flex-col gap-3"> {/* Reduced gap */}
{/* Main Progress Chart */}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-4">
<div className="flex flex-col items-center mb-2">
<Title className="text-gray-800 text-base font-semibold text-center">
Project Progress by Stage
</Title>
<p className="text-xs text-gray-500 text-center 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-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>
<div className="w-full max-w-md">
<BarList
data={normalStages}
color="blue"
@ -122,19 +179,21 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</Card>
</div>
</Card>
{/* Exception Chart */}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-4">
<div className="flex flex-col items-center mb-2">
<Title className="text-gray-800 text-base font-semibold text-center">
Needs HA Support & Not Viable
</Title>
<p className="text-xs text-gray-500 text-center mt-0.5">
Click to explore exceptions
</p>
</div>
{/* 🚨 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>
<div className="w-full max-w-md">
<BarList
data={exceptionStages}
color="red"
@ -142,7 +201,8 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</Card>
</div>
);
</div>
</Card>
</div>
);
}

View file

@ -1,7 +1,7 @@
"use client";
import { DonutChart, Card, Title } from "@tremor/react";
import { useMemo } from "react";
import { useMemo, useState } from "react";
interface SurveyedPieChartProps {
deals: Record<string, any>[];
@ -33,6 +33,7 @@ export default function SurveyedPieChart({
"slate-400",
"gray-300",
"gray-100",
"gray-200",
];
const data = useMemo(() => {
@ -57,55 +58,97 @@ export default function SurveyedPieChart({
onOpenTable?.(value.name, filteredDeals);
};
const [hovered, setHovered] = useState<string | null>(null);
return (
<Card className="flex flex-col items-center p-6 pt-10 pb-8">
{/* Header */}
<div className="text-center mb-4">
<Title className="text-gray-800 text-[15px] font-semibold tracking-tight">
Survey Performance
</Title>
<p className="text-xs text-gray-500 mt-1">
Click a segment or label to view filtered properties
</p>
</div>
{/* Chart */}
<div className="relative flex justify-center items-center mt-8">
<DonutChart
data={data}
category="amount"
index="name"
valueFormatter={(n) => `${n.toLocaleString()}`}
colors={colors}
onValueChange={handleClick}
showLabel={false}
className="w-64 h-64 cursor-pointer"
customTooltip={({ payload }) => {
const item = payload?.[0]?.payload;
if (!item) return null;
const { name, amount } = item;
return (
<div
className="bg-white/80 backdrop-blur-md px-4 py-2.5 rounded-lg shadow-md
border border-gray-200 text-gray-800 text-sm font-medium"
>
<div className="flex flex-col items-center">
<span className="text-[0.95rem] font-semibold text-gray-900">{name}</span>
<span className="opacity-70">{amount.toLocaleString()}</span>
</div>
</div>
);
}}
/>
{data.length > 0 && (
<div className="absolute text-center">
<span className="text-3xl font-semibold text-gray-800">
{data.reduce((a, b) => a + b.amount, 0)}
</span>
<Card className="flex flex-col items-center p-6 pt-10 pb-8 bg-white">
{/* Header */}
<div className="text-center mb-4">
<Title className="text-gray-800 text-[15px] font-semibold tracking-tight">
Survey Performance
</Title>
<p className="text-xs text-gray-500 mt-1">
Click a segment or label to view filtered properties
</p>
</div>
)}
</div>
</Card>
{/* Donut Chart (Centered) */}
<div className="relative flex justify-center items-center mt-6">
<DonutChart
data={data}
category="amount"
index="name"
valueFormatter={(n) => `${n.toLocaleString()}`}
colors={colors}
onValueChange={handleClick}
showLabel={false}
className="w-64 h-64 cursor-pointer"
customTooltip={({ payload }) => {
const item = payload?.[0]?.payload;
if (!item) return null;
const { name, amount } = item;
return (
<div
className="bg-white/80 backdrop-blur-md px-4 py-2.5 rounded-lg shadow-md
border border-gray-200 text-gray-800 text-sm font-medium"
>
<div className="flex flex-col items-center">
<span className="text-[0.95rem] font-semibold text-gray-900">
{name}
</span>
<span className="opacity-70">{amount.toLocaleString()}</span>
</div>
</div>
);
}}
/>
{data.length > 0 && (
<div className="absolute text-center">
<span className="text-3xl font-semibold text-gray-800">
{data.reduce((a, b) => a + b.amount, 0)}
</span>
</div>
)}
</div>
{/* Legend (Clean Grid Layout) */}
<div className="mt-8 flex flex-wrap justify-center gap-x-6 gap-y-3 max-w-[90%]">
{data.map((item, idx) => (
<div
key={item.name}
onClick={() => 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"
>
<span
className={`inline-block w-3.5 h-3.5 rounded-full bg-${colors[idx]} border border-gray-300 flex-shrink-0`}
/>
<span className="font-medium truncate max-w-[110px]">
{item.name}
</span>
<span className="text-xs text-gray-500 ml-1 whitespace-nowrap">
{item.percentage}%
</span>
{/* Tooltip on hover */}
{hovered === item.name && (
<div
className="absolute -top-11 left-1/2 -translate-x-1/2 bg-white/80 backdrop-blur-md
px-4 py-2.5 rounded-lg shadow-md border border-gray-200 text-gray-800
text-sm font-medium whitespace-nowrap z-20"
>
<div className="flex flex-col items-center">
<span className="text-[0.95rem] font-semibold text-gray-900">
{item.name}
</span>
<span className="opacity-70">{item.amount.toLocaleString()}</span>
</div>
</div>
)}
</div>
))}
</div>
</Card>
);
}