mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge pull request #122 from Hestia-Homes/feature/major_condition_issue
Feature/major condition issue
This commit is contained in:
commit
2d69cbdcb3
6 changed files with 302 additions and 115 deletions
|
|
@ -15,6 +15,9 @@ export const hubspotDealData = pgTable("hubspot_deal_data", {
|
|||
outcome: text("outcome"),
|
||||
outcomeNotes: text("outcome_notes"),
|
||||
|
||||
majorConditionIssueDescription: text("major_condition_issue_description"),
|
||||
majorConditionIssuePhotos: text("major_condition_issue_photos"),
|
||||
|
||||
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
|
||||
/* 🌞 Light Theme (raw HSL values) */
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import EmailSignInButton from "./components/signin/EmailSignInButton";
|
|||
import { redirect } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
|
||||
|
||||
export default async function Home(props: {
|
||||
searchParams: Promise<{ error?: string }>;
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -7,121 +7,173 @@ const STAGE_ORDER = [
|
|||
"Initial planning",
|
||||
"Booking team to contact tenant",
|
||||
"Survey in progress",
|
||||
"Not viable",
|
||||
"Needs Heating Upgrade installed",
|
||||
"Needs support",
|
||||
"Coordination + design",
|
||||
"Ready to be installed",
|
||||
"Ready for install",
|
||||
"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("Booking team to contact tenant"),
|
||||
"1984401629": stage("Booking team to contact tenant"),
|
||||
"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("Survey in progress"),
|
||||
"2571417798": stage("Survey in progress"),
|
||||
"1887736000": stage("Survey in progress"),
|
||||
"1617223916": stage("Survey in progress"),
|
||||
"2628341989": stage("Survey in progress"),
|
||||
"3441170637": stage("Survey in progress"),
|
||||
"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
|
||||
|
||||
"1887735998": stage("Not viable"),
|
||||
"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
|
||||
|
||||
"3061261536": stage("Needs Heating Upgrade installed"),
|
||||
"1887735999": stage("Needs Heating Upgrade installed"),
|
||||
"3016601828": stage("Needs Heating Upgrade installed"),
|
||||
"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
|
||||
|
||||
"1617223914": stage("Needs support"),
|
||||
"2628233422": stage("Needs support"),
|
||||
"2702650617": stage("Needs support"),
|
||||
"2473886962": stage("Needs support"),
|
||||
|
||||
"1668803774": stage("Coordination + design"),
|
||||
"3440363736": stage("Coordination + design"),
|
||||
"2769407183": stage("Needs Heating Upgrade installed"),
|
||||
"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) {
|
||||
const data = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
// Count deals by stage
|
||||
deals.forEach((d) => {
|
||||
const stageId = d.dealstage || "unknown";
|
||||
const stageName = STAGE_LABELS[stageId] || "Unknown Stage";
|
||||
counts[stageName] = (counts[stageName] || 0) + 1;
|
||||
});
|
||||
|
||||
// Ensure every stage in STAGE_ORDER has a default of 0
|
||||
const complete = STAGE_ORDER.map((name) => ({
|
||||
return STAGE_ORDER.map((name) => ({
|
||||
name,
|
||||
value: counts[name] || 0,
|
||||
}));
|
||||
|
||||
// Sort according to STAGE_ORDER (just in case)
|
||||
return complete.sort((a, b) => {
|
||||
const aIndex = STAGE_ORDER.indexOf(a.name);
|
||||
const bIndex = STAGE_ORDER.indexOf(b.name);
|
||||
return (
|
||||
(aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
|
||||
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex)
|
||||
);
|
||||
});
|
||||
}, [deals]);
|
||||
|
||||
// ✅ Calculate total 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>);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg mx-auto bg-white rounded-2xl shadow-md hover:shadow-lg transition-all duration-200 p-6">
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<Title className="text-gray-800 text-lg font-semibold tracking-tight text-center">
|
||||
// Split into normal + exception stages
|
||||
const normalStages = data.filter(
|
||||
(d) =>
|
||||
!["Needs support from HA", "Not viable for funding"].includes(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">
|
||||
{/* ✅ 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-sm text-gray-500 text-center mt-1">
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Click a bar to view related properties
|
||||
</p>
|
||||
|
||||
{/* ✅ Total count */}
|
||||
<p className="text-sm text-gray-700 font-medium mt-2">
|
||||
<p className="text-xs text-gray-700 font-medium mt-1">
|
||||
Total: {total.toLocaleString()} properties
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="w-full max-w-md">
|
||||
<BarList
|
||||
data={data}
|
||||
data={normalStages}
|
||||
color="blue"
|
||||
sortOrder="none"
|
||||
className="cursor-pointer"
|
||||
|
|
@ -129,5 +181,28 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
|
|||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<BarList
|
||||
data={exceptionStages}
|
||||
color="red"
|
||||
sortOrder="none"
|
||||
className="cursor-pointer"
|
||||
onValueChange={handleBarClick}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ import {
|
|||
CardTitle,
|
||||
CardContent,
|
||||
} from "@/app/shadcn_components/ui/card";
|
||||
import { Home, AlertTriangle, BarChart3 } from "lucide-react";
|
||||
import { Home, AlertTriangle } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface ReportsProps {
|
||||
deals: Record<string, any>[];
|
||||
}
|
||||
|
||||
const MAJOR_CONDITION_STAGE_ID = "3061261536";
|
||||
|
||||
export default function LiveTracker({ deals }: ReportsProps) {
|
||||
|
|
@ -32,7 +31,10 @@ export default function LiveTracker({ deals }: ReportsProps) {
|
|||
const [openTable, setOpenTable] = useState<{
|
||||
stage: string;
|
||||
data: any[];
|
||||
columns: string[];
|
||||
columnLabels: Record<string, string>;
|
||||
} | null>(null);
|
||||
|
||||
const projectCodes = Object.keys(groupedDeals);
|
||||
const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]);
|
||||
const currentDeals = groupedDeals[currentProjectCode];
|
||||
|
|
@ -43,8 +45,23 @@ export default function LiveTracker({ deals }: ReportsProps) {
|
|||
const majorIssues = majorConditionDeals.length;
|
||||
const majorPercent = ((majorIssues / totalProperties) * 100).toFixed(1);
|
||||
|
||||
const handleOpenTable = (stage: string, filteredDeals: any[]) => {
|
||||
setOpenTable({ stage, data: filteredDeals });
|
||||
const handleOpenTable = (
|
||||
stage: string,
|
||||
filteredDeals: any[],
|
||||
columns?: string[],
|
||||
columnLabels?: Record<string, string>
|
||||
) => {
|
||||
setOpenTable({
|
||||
stage,
|
||||
data: filteredDeals,
|
||||
columns:
|
||||
columns || ["dealname", "landlordPropertyId"],
|
||||
columnLabels:
|
||||
columnLabels || {
|
||||
dealname: "Address Ref.",
|
||||
landlordPropertyId: "Property Ref.",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!deals?.length) {
|
||||
|
|
@ -66,18 +83,42 @@ export default function LiveTracker({ deals }: ReportsProps) {
|
|||
icon={Home}
|
||||
title="Total Properties"
|
||||
value={totalProperties}
|
||||
onClick={() => handleOpenTable("All Properties", deals)}
|
||||
onClick={() =>
|
||||
handleOpenTable(
|
||||
"All Properties",
|
||||
deals,
|
||||
["dealname", "landlordPropertyId", "projectCode"],
|
||||
{
|
||||
dealname: "Address Ref.",
|
||||
landlordPropertyId: "Property Ref.",
|
||||
projectCode: "Project Code",
|
||||
}
|
||||
)
|
||||
}
|
||||
accent="brandblue"
|
||||
/>
|
||||
|
||||
{/* Major Issues */}
|
||||
<StatCard
|
||||
icon={AlertTriangle}
|
||||
title="Major Condition Issues"
|
||||
value={`${majorIssues} `}
|
||||
title="Awaab's Law Reporting"
|
||||
value={`${majorIssues}`}
|
||||
subtitle={`(${majorPercent}%)`}
|
||||
onClick={() =>
|
||||
handleOpenTable("Major Condition Issues", majorConditionDeals)
|
||||
handleOpenTable(
|
||||
"Awaab's Law Reporting",
|
||||
majorConditionDeals,
|
||||
[
|
||||
"dealname",
|
||||
"landlordPropertyId",
|
||||
"majorConditionIssueDescription",
|
||||
],
|
||||
{
|
||||
dealname: "Address Ref.",
|
||||
landlordPropertyId: "Property Ref.",
|
||||
majorConditionIssueDescription: "Surveyor's Notes"
|
||||
}
|
||||
)
|
||||
}
|
||||
accent="red"
|
||||
/>
|
||||
|
|
@ -155,18 +196,8 @@ export default function LiveTracker({ deals }: ReportsProps) {
|
|||
<div className="flex-1 overflow-auto">
|
||||
<TableViewer
|
||||
data={openTable.data}
|
||||
columns={[
|
||||
"dealname",
|
||||
"landlordPropertyId",
|
||||
"outcome",
|
||||
"outcomeNotes",
|
||||
]}
|
||||
columnLabels={{
|
||||
dealname: "Address Ref.",
|
||||
landlordPropertyId: "Property Ref.",
|
||||
outcome: "Outcome",
|
||||
outcomeNotes: "Notes from Surveyor",
|
||||
}}
|
||||
columns={openTable.columns}
|
||||
columnLabels={openTable.columnLabels}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -185,7 +216,7 @@ export default function LiveTracker({ deals }: ReportsProps) {
|
|||
);
|
||||
}
|
||||
|
||||
/** 🔸Small stat card to match DashboardSummary visuals */
|
||||
/** 🔸Small stat card component */
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -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>[];
|
||||
|
|
@ -24,6 +24,18 @@ export default function SurveyedPieChart({
|
|||
"Rescheduled",
|
||||
];
|
||||
|
||||
const colors = [
|
||||
"indigo-600",
|
||||
"indigo-400",
|
||||
"blue-300",
|
||||
"amber-400",
|
||||
"amber-200",
|
||||
"slate-400",
|
||||
"gray-300",
|
||||
"gray-100",
|
||||
"gray-200",
|
||||
];
|
||||
|
||||
const data = useMemo(() => {
|
||||
const outcomeCounts: Record<string, number> = {};
|
||||
deals.forEach((deal) => {
|
||||
|
|
@ -32,9 +44,11 @@ export default function SurveyedPieChart({
|
|||
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
||||
}
|
||||
});
|
||||
const total = Object.values(outcomeCounts).reduce((a, b) => a + b, 0);
|
||||
return Object.entries(outcomeCounts).map(([name, amount]) => ({
|
||||
name,
|
||||
amount,
|
||||
percentage: total ? ((amount / total) * 100).toFixed(1) : "0.0",
|
||||
}));
|
||||
}, [deals]);
|
||||
|
||||
|
|
@ -44,35 +58,96 @@ export default function SurveyedPieChart({
|
|||
onOpenTable?.(value.name, filteredDeals);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg mx-auto bg-white rounded-2xl shadow-md hover:shadow-lg transition-all duration-200 p-6">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Title className="text-gray-800 text-lg font-semibold tracking-tight text-center">
|
||||
Survey Outcomes
|
||||
</Title>
|
||||
<p className="text-sm text-gray-500 text-center -mt-2">
|
||||
Click a segment to view filtered properties
|
||||
</p>
|
||||
const [hovered, setHovered] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
{/* 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={[
|
||||
"#2d348f",
|
||||
"#14163d",
|
||||
"#3943b7",
|
||||
"#5d6be0",
|
||||
"black",
|
||||
"#eff6fc",
|
||||
"lightBlue",
|
||||
"navy",
|
||||
"azure",
|
||||
]}
|
||||
className="w-64 h-64 cursor-pointer transition-transform hover:scale-[1.03]"
|
||||
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