mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
push to production
This commit is contained in:
parent
ece75781b9
commit
b3253d7e84
3 changed files with 224 additions and 122 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue