show demo to harry

This commit is contained in:
Jun-te Kim 2026-02-25 14:53:51 +00:00
parent b1ce906f2f
commit 694871a36b
6 changed files with 590 additions and 363 deletions

View file

@ -1,71 +1,6 @@
"use client";
import { BarList, Card, Title } from "@tremor/react";
import ExpandableCountBar from "./ExpandableCountBar";
/* ================================
STAGE ORDER
================================ */
const STAGE_ORDER = [
"Initial planning",
"Booking team to contact tenant",
"In Assessment",
"In Coordination",
"In Design",
"Completed",
"Queries",
];
const stage = (label: string) =>
STAGE_ORDER.find((s) => s === label)!;
/* ================================
AFTER ASSESSMENT LOGIC
================================ */
const getAfterAssessmentLabel = (
coordinationStatus?: string,
designStatus?: string
): string => {
const coordStatusUpper = coordinationStatus?.toUpperCase() ?? "";
const designStatusUpper = designStatus?.toUpperCase() ?? "";
if (coordStatusUpper === "RA ISSUE") return "Queries";
if (
coordStatusUpper.includes("(V1) IOE/MTP COMPLETE") ||
coordStatusUpper.includes("(V2) IOE/MTP COMPLETE") ||
coordStatusUpper.includes("(V3) IOE/MTP COMPLETE")
) {
if (designStatusUpper === "UPLOADED") return "Completed";
return "In Design";
}
return "In Coordination";
};
/* ================================
STAGE LABELS
================================ */
const STAGE_LABELS: Record<string, string> = {
"1617223910": stage("Initial planning"),
"3583836399": stage("Initial planning"),
"3589581001": stage("Booking team to contact tenant"),
"1984401629": stage("In Assessment"),
"2628233422": "AFTER_ASSESSMENT",
"2702650617": "AFTER_ASSESSMENT",
"2473886962": "AFTER_ASSESSMENT",
"1668803774": "AFTER_ASSESSMENT",
"1887735998": stage("Queries"),
};
/* ================================
TYPES
================================ */
import ProgressOverview from "./ProgressOverview";
interface Deal {
dealname: string;
@ -80,199 +15,19 @@ interface DealStageChartProps {
deals: Deal[];
onOpenTable?: (
stageName: string,
filteredDeals: Deal[]
filteredDeals: Deal[],
columns?: string[],
columnLabels?: Record<string, string>,
breakdown?: Record<string, Deal[]>
) => void;
}
/* ================================
STAGE RESOLUTION ENGINE
================================ */
const resolveDisplayStage = (deal: Deal): string => {
let stageName = STAGE_LABELS[deal.dealstage] || "Unknown Stage";
if (stageName === "AFTER_ASSESSMENT") {
stageName =
getAfterAssessmentLabel(
deal.coordinationStatus,
deal.designStatus
) || "In Coordination";
}
if (stageName === "Initial planning") {
const coordStatusUpper =
deal.coordinationStatus?.toUpperCase() ?? "";
if (coordStatusUpper === "RA ISSUE") {
stageName = "Queries";
}
}
return stageName;
};
/* ================================
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;
/* ---------- Shared Summary Click Handler ---------- */
const handleSummaryClick = (
label: string,
stages: string[]
) => {
const filteredDeals = getDealsByResolvedStage(
deals,
stages
);
onOpenTable?.(label, filteredDeals);
};
/* ---------- Stage Bar Click ---------- */
const handleBarClick = (value: {
name: string;
value: number;
}) => {
const filteredDeals = getDealsByResolvedStage(
deals,
[value.name]
);
onOpenTable?.(value.name, filteredDeals);
};
/* ---------- Split Normal vs Exception ---------- */
const normalStages = data.filter(
(d) => d.name !== "Queries"
);
const exceptionStages = data.filter(
(d) => d.name === "Queries"
);
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>
<BarList
data={normalStages}
color="blue"
sortOrder="none"
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</Card>
{/* ===== Queries / 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">
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>
<ProgressOverview deals={deals} onOpenTable={onOpenTable} />
);
}

View file

@ -3,6 +3,11 @@
interface ExpandableCountBarProps<T> {
title: string
items: T[]
count?: number
percentage?: number
inProgressPercentage?: number
total?: number
secondaryStats?: Array<{ label: string; count: number }>
onClick?: (items: T[]) => void
className?: string
}
@ -10,25 +15,106 @@ interface ExpandableCountBarProps<T> {
export default function ExpandableCountBar<T>({
title,
items,
count,
percentage,
inProgressPercentage,
total,
secondaryStats,
onClick,
className = "",
}: ExpandableCountBarProps<T>) {
const count = items.length
const displayCount = count ?? items.length
const displayTotal = total ?? items.length
const displayPercentage = percentage ?? 0
const displayInProgressPercentage = inProgressPercentage ?? 0
const radius = 40
const circumference = 2 * Math.PI * radius
const completedStrokeDashoffset = circumference - (displayPercentage / 100) * circumference
const inProgressStrokeDashoffset = circumference - ((displayPercentage + displayInProgressPercentage) / 100) * circumference
return (
<div
<button
onClick={() => onClick?.(items)}
className={`w-full cursor-pointer rounded-xl border bg-white shadow-sm hover:shadow-md transition-all duration-200 p-5 flex justify-between items-center ${className}`}
className={`w-full cursor-pointer rounded-xl border border-brandblue/20 bg-gradient-to-br from-brandlightblue/20 to-brandlightblue/5 shadow-sm hover:shadow-md hover:border-brandblue/40 transition-all duration-300 p-6 flex flex-col gap-4 group active:scale-95 ${className}`}
>
<div className="text-base font-semibold text-gray-800">
{title}
<div className="flex items-center justify-between gap-4">
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-brandblue group-hover:text-brandmidblue transition-colors">
{title}
</p>
<p className="text-xs text-gray-500 mt-1">Click to view breakdown</p>
</div>
{/* Circular Progress */}
<div className="relative w-24 h-24 flex-shrink-0">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
{/* Background circle */}
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="3"
className="text-brandblue/10"
/>
{/* In Progress circle (gold) - drawn first so it appears underneath */}
{displayInProgressPercentage > 0 && (
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="#f59e0b"
strokeWidth="3"
strokeDasharray={circumference}
strokeDashoffset={inProgressStrokeDashoffset}
strokeLinecap="round"
className="transition-all duration-700 ease-out"
/>
)}
{/* Completed progress circle (blue) - drawn last so it appears on top */}
{displayPercentage > 0 && (
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="#14163d"
strokeWidth="3"
strokeDasharray={circumference}
strokeDashoffset={completedStrokeDashoffset}
strokeLinecap="round"
className="transition-all duration-700 ease-out"
/>
)}
</svg>
{/* Center text */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-lg font-bold text-brandblue">
{displayPercentage.toFixed(0)}%
</span>
<span className="text-xs text-gray-600 font-semibold">
{displayCount}/{displayTotal}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
<span>{count} items</span>
<span className="text-xs"></span>
</div>
</div>
{/* Secondary Stats */}
{secondaryStats && secondaryStats.length > 0 && (
<div className="grid grid-cols-2 gap-2 pt-3 border-t border-brandblue/10">
{secondaryStats.map((stat) => (
<div key={stat.label} className="text-left">
<p className="text-xs text-gray-600 font-medium mb-1">{stat.label}</p>
<p className="text-lg font-bold text-brandblue">{stat.count}</p>
</div>
))}
</div>
)}
<span className="text-brandblue/40 group-hover:text-brandblue/70 text-xl transition-colors transform group-hover:-rotate-180 duration-300 self-end"></span>
</button>
)
}

View file

@ -0,0 +1,347 @@
"use client";
import { Card, Title } from "@tremor/react";
import { AlertCircle } from "lucide-react";
import { motion } from "framer-motion";
import ExpandableCountBar from "./ExpandableCountBar";
interface ProgressOverviewProps {
deals: Record<string, any>[];
onOpenTable?: (
stage: string,
deals: any[],
columns?: string[],
columnLabels?: Record<string, string>,
breakdown?: Record<string, any[]>
) => void;
}
export default function ProgressOverview({ deals, onOpenTable }: ProgressOverviewProps) {
const STAGE_ORDER = [
"Scope & Planning",
"Booking in Progress",
"Assessment in Progress",
"Coordination in Progress",
"Design in Progress",
"Completed",
];
const STAGE_LABELS: Record<string, string> = {
"1617223910": "Scope & Planning",
"3583836399": "Scope & Planning",
"3589581001": "Booking in Progress",
"1984401629": "Assessment in Progress",
"2628233422": "AFTER_ASSESSMENT",
"2702650617": "AFTER_ASSESSMENT",
"2473886962": "AFTER_ASSESSMENT",
"1668803774": "AFTER_ASSESSMENT",
"1887735998": "Queries",
};
const getAfterAssessmentLabel = (
coordinationStatus?: string,
designStatus?: string
): string => {
const coordStatusUpper = coordinationStatus?.toUpperCase() ?? "";
const designStatusUpper = designStatus?.toUpperCase() ?? "";
if (coordStatusUpper === "RA ISSUE") return "Queries";
if (
coordStatusUpper.includes("(V1) IOE/MTP COMPLETE") ||
coordStatusUpper.includes("(V2) IOE/MTP COMPLETE") ||
coordStatusUpper.includes("(V3) IOE/MTP COMPLETE")
) {
if (designStatusUpper === "UPLOADED") return "Completed";
return "Design in Progress";
}
return "Coordination in Progress";
};
const resolveDisplayStage = (deal: Record<string, any>): string => {
let stageName = STAGE_LABELS[deal.dealstage] || "Unknown Stage";
if (stageName === "AFTER_ASSESSMENT") {
stageName =
getAfterAssessmentLabel(
deal.coordinationStatus,
deal.designStatus
) || "Coordination in Progress";
}
if (stageName === "Scope & Planning") {
const coordStatusUpper = deal.coordinationStatus?.toUpperCase() ?? "";
if (coordStatusUpper === "RA ISSUE") {
stageName = "Queries";
}
}
if (stageName === "Assessment in Progress") {
const coordStatusUpper = deal.coordinationStatus?.toUpperCase() ?? "";
if (coordStatusUpper === "RA ISSUE") {
stageName = "Queries";
}
}
return stageName;
};
// Separate queries from main stages
const queriesDealsList = deals.filter((deal) => resolveDisplayStage(deal) === "Queries");
// Calculate stage distribution (excluding queries)
const stageCounts: Record<string, number> = {};
const stageDeals: Record<string, any[]> = {};
deals.forEach((deal) => {
const stage = resolveDisplayStage(deal);
if (stage !== "Queries") {
stageCounts[stage] = (stageCounts[stage] || 0) + 1;
if (!stageDeals[stage]) stageDeals[stage] = [];
stageDeals[stage].push(deal);
}
});
const total = Object.values(stageCounts).reduce((a, b) => a + b, 0);
const completedCount = stageCounts["Completed"] || 0;
const completedPercentage = total > 0 ? (completedCount / total) * 100 : 0;
// Calculate progress for each stage
const stageProgress = STAGE_ORDER.filter((s) => s !== "Queries").map((stage) => {
const count = stageCounts[stage] || 0;
const percentage = total > 0 ? (count / total) * 100 : 0;
return { stage, count, percentage, deals: stageDeals[stage] || [] };
});
const handleStageClick = (item: any) => {
if (onOpenTable && item.deals.length > 0) {
onOpenTable(item.stage, item.deals, ["dealname", "landlordPropertyId"], {
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
});
}
};
const handleQueriesClick = () => {
if (onOpenTable && queriesDealsList.length > 0) {
onOpenTable(
"Properties Needing Attention",
queriesDealsList,
["dealname", "landlordPropertyId", "coordinationStatus"],
{
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
coordinationStatus: "Issue",
}
);
}
};
const radius = 40
const circumference = 2 * Math.PI * radius
const strokeDashoffset = circumference - (completedPercentage / 100) * circumference
// Coordination and Design calculations
const completedDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Completed");
const coordinationInProgressDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Coordination in Progress");
const coordinationCompletedDeals = deals.filter((deal) => {
const stage = resolveDisplayStage(deal);
return stage === "Design in Progress" || stage === "Completed";
});
const designInProgressDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Design in Progress");
const designCompletedDeals = deals.filter((deal) => resolveDisplayStage(deal) === "Completed");
const totalDeals = deals.length;
const coordCompletedPercentage = totalDeals > 0 ? (coordinationCompletedDeals.length / totalDeals) * 100 : 0;
const coordInProgressPercentage = totalDeals > 0 ? (coordinationInProgressDeals.length / totalDeals) * 100 : 0;
const designCompletedPercentage = totalDeals > 0 ? (designCompletedDeals.length / totalDeals) * 100 : 0;
const designInProgressPercentageVal = totalDeals > 0 ? (designInProgressDeals.length / totalDeals) * 100 : 0;
const coordinationBreakdown = {
"Coordination Completed": coordinationCompletedDeals,
"Coordination in Progress": coordinationInProgressDeals,
};
const handleSummaryClick = (
label: string,
stages: string[],
breakdown?: Record<string, any[]>
) => {
const filteredDeals = deals.filter((deal) => {
const stage = resolveDisplayStage(deal);
return stages.includes(stage);
});
onOpenTable?.(label, filteredDeals, undefined, undefined, breakdown);
};
return (
<div className="space-y-6">
{/* Work Completed - Full Width Overview at Top */}
<motion.button
onClick={() => {
if (onOpenTable && completedDeals.length > 0) {
onOpenTable("Completed Properties", completedDeals, ["dealname", "landlordPropertyId"], {
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
});
}
}}
whileHover={{ scale: 1.02 }}
className="group relative text-left w-full"
>
<Card className="bg-gradient-to-br from-emerald-50/80 to-emerald-50/40 border-2 border-emerald-300/60 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 p-6 hover:border-emerald-400">
<div className="space-y-4">
{/* Header with Circular Progress */}
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<p className="text-sm font-bold text-emerald-900 uppercase tracking-wide">
Work Completed
</p>
<p className="text-xs text-emerald-700 mt-1">
End-to-end project overview
</p>
</div>
{/* Circular Progress */}
<div className="relative w-24 h-24 flex-shrink-0">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
{/* Background circle */}
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="3"
className="text-emerald-200"
/>
{/* Progress circle */}
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="url(#completedGradient)"
strokeWidth="3"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className="transition-all duration-700 ease-out"
/>
<defs>
<linearGradient id="completedGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#059669" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
</svg>
{/* Center text */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-lg font-bold text-emerald-700">
{completedPercentage.toFixed(0)}%
</span>
<span className="text-xs text-emerald-600 font-semibold">
{completedCount}/{totalDeals}
</span>
</div>
</div>
</div>
{/* CTA */}
<div className="flex items-center gap-2 text-emerald-700 group-hover:text-emerald-900 transition-colors pt-2 border-t border-emerald-200/50">
<span className="text-sm font-semibold">View Completed Properties</span>
<span className="text-lg"></span>
</div>
</div>
</Card>
</motion.button>
{/* Project Summary Cards - Coordination & Design */}
<div className="grid grid-cols-2 gap-3">
<ExpandableCountBar
title="Coordination Completed"
count={coordinationCompletedDeals.length}
percentage={coordCompletedPercentage}
inProgressPercentage={coordInProgressPercentage}
total={totalDeals}
secondaryStats={[
{ label: "Coordination Completed", count: coordinationCompletedDeals.length },
{ label: "Coordination in Progress", count: coordinationInProgressDeals.length },
]}
items={coordinationCompletedDeals.concat(coordinationInProgressDeals)}
onClick={() =>
handleSummaryClick(
"Coordination Status",
["Design in Progress", "Completed", "Coordination in Progress"],
coordinationBreakdown
)
}
/>
<ExpandableCountBar
title="Design Completed"
count={designCompletedDeals.length + designInProgressDeals.length}
percentage={designCompletedPercentage}
inProgressPercentage={designInProgressPercentageVal}
total={totalDeals}
secondaryStats={[
{ label: "Design Completed", count: designCompletedDeals.length },
{ label: "Design in Progress", count: designInProgressDeals.length },
]}
items={designCompletedDeals.concat(designInProgressDeals)}
onClick={() =>
handleSummaryClick(
"Design Status",
["Design in Progress", "Completed"]
)
}
/>
</div>
{/* Queries / Attention Required Section */}
{queriesDealsList.length > 0 && (
<motion.button
onClick={handleQueriesClick}
whileHover={{ scale: 1.02 }}
className="group relative text-left w-full"
>
<Card className="bg-gradient-to-br from-amber-50/80 to-amber-50/40 border-2 border-amber-300/60 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 p-6 hover:border-amber-400">
<div className="space-y-4">
{/* Header with Alert */}
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-0.5">
<AlertCircle className="w-6 h-6 text-amber-600 animate-pulse" />
</div>
<div className="flex-1">
<p className="text-sm font-bold text-amber-900 uppercase tracking-wide">
Requires Your Input
</p>
<p className="text-xs text-amber-700 mt-1">
These properties need your feedback or assistance to progress
</p>
</div>
</div>
{/* Count Display */}
<div className="pt-3 border-t border-amber-200/50">
<p className="text-4xl font-black text-amber-600 mb-1">
{queriesDealsList.length}
</p>
<p className="text-xs font-semibold text-amber-700 opacity-70">
{queriesDealsList.length === 1 ? "property" : "properties"} awaiting action
</p>
</div>
{/* CTA */}
<div className="flex items-center gap-2 text-amber-700 group-hover:text-amber-900 transition-colors">
<span className="text-sm font-semibold">Review Details</span>
<span className="text-lg"></span>
</div>
</div>
</Card>
</motion.button>
)}
</div>
);
}

View file

@ -4,12 +4,7 @@ import { useState } from "react";
import { DealStageChart } from "./DealStageChart";
import SurveyedPieChart from "./SurveyedResultsPieChart";
import TableViewer from "./TableViewer";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/app/shadcn_components/ui/card";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { Home, AlertTriangle } from "lucide-react";
import { motion } from "framer-motion";
@ -35,6 +30,7 @@ export default function LiveTracker({ deals }: ReportsProps) {
data: any[];
columns: string[];
columnLabels: Record<string, string>;
breakdown?: Record<string, any[]>;
} | null>(null);
const projectCodes = Object.keys(groupedDeals);
@ -67,7 +63,8 @@ export default function LiveTracker({ deals }: ReportsProps) {
stage: string,
filteredDeals: any[],
columns?: string[],
columnLabels?: Record<string, string>
columnLabels?: Record<string, string>,
breakdown?: Record<string, any[]>
) => {
setOpenTable({
stage,
@ -79,23 +76,50 @@ export default function LiveTracker({ deals }: ReportsProps) {
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
},
breakdown,
});
};
if (!deals?.length) {
return (
<Card className="p-8 text-center bg-gradient-to-br from-white to-gray-50 border border-gray-100 shadow-sm">
<Card className="p-8 text-center bg-gradient-to-br from-brandlightblue/30 to-white border border-brandblue/10 shadow-sm">
<CardContent>
<p className="text-gray-500 text-sm">No deal data available.</p>
<p className="text-gray-600 text-sm">No deal data available.</p>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4 w-full">
<div className="space-y-6 w-full">
{/* 🌍 Global Overview */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Project Selector */}
<Card className="flex flex-col justify-center items-center border border-brandblue/10 bg-gradient-to-br from-brandlightblue/20 to-white shadow-sm hover:shadow-md transition-shadow p-5">
<div className="w-full flex flex-col">
<p className="text-xs uppercase tracking-wide text-gray-600 mb-3 font-semibold">
Select Project
</p>
<div className="relative">
<select
id="projectSelect"
value={currentProjectCode}
onChange={(e) => setCurrentProjectCode(e.target.value)}
className="w-full px-4 py-2.5 pr-10 border border-brandblue/20 rounded-lg bg-white text-gray-800 font-medium text-center focus:ring-2 focus:ring-brandblue focus:border-brandblue focus:outline-none transition-all appearance-none"
>
{projectCodes.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
<div className="absolute right-2 top-3 text-brandblue pointer-events-none opacity-60 text-xs">
</div>
</div>
</div>
</Card>
{/* Total Properties */}
<StatCard
icon={Home}
@ -140,52 +164,22 @@ export default function LiveTracker({ deals }: ReportsProps) {
}
)
}
accent="red"
accent={majorIssues > 0 ? "red" : "brandblue"}
/>
{/* Project Selector */}
<Card className="flex flex-col justify-center items-center border border-gray-100 bg-gradient-to-br from-white to-gray-50 shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="font-normal">
<p className="text-xs uppercase text-gray-500 mb-1">
Select Project
</p>
</CardTitle>
</CardHeader>
<CardContent>
<div className="relative w-56">
<select
id="projectSelect"
value={currentProjectCode}
onChange={(e) => setCurrentProjectCode(e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white text-gray-800 focus:ring-2 focus:ring-brandblue focus:outline-none"
>
{projectCodes.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
<div className="absolute right-3 top-2.5 text-gray-400 pointer-events-none">
</div>
</div>
</CardContent>
</Card>
</div>
{/* 📊 Project Insights */}
<Card className="border border-gray-100 bg-gradient-to-br from-white to-gray-50 shadow-md">
<CardHeader>
<CardTitle className="text-center text-lg font-semibold text-brandblue tracking-tight">
Project-Level Insights {currentProjectCode}
</CardTitle>
</CardHeader>
<div>
<div className="mb-6 pb-4 border-b border-brandblue/20 text-center">
<h2 className="text-lg font-bold text-brandblue break-words">
Project-Level Insights <span className="text-brandmidblue">{currentProjectCode}</span>
</h2>
</div>
<CardContent className={`grid gap-6 ${hasSurveyData ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1 max-w-2xl mx-auto"}`}>
<div className={`grid gap-6 ${hasSurveyData ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1 max-w-3xl mx-auto"}`}>
<motion.div
whileHover={{ scale: 1.01 }}
className="border rounded-xl p-5 bg-white shadow-sm hover:shadow-md transition"
className="transition-all duration-300"
>
<DealStageChart
deals={currentDeals}
@ -196,7 +190,7 @@ export default function LiveTracker({ deals }: ReportsProps) {
{hasSurveyData && (
<motion.div
whileHover={{ scale: 1.01 }}
className="border rounded-xl p-5 bg-white shadow-sm hover:shadow-md transition"
className="transition-all duration-300"
>
<SurveyedPieChart
deals={currentDeals}
@ -204,18 +198,47 @@ export default function LiveTracker({ deals }: ReportsProps) {
/>
</motion.div>
)}
</CardContent>
</Card>
</div>
</div>
{/* 🔹 Table Modal */}
{openTable && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-opacity">
<div className="bg-white rounded-2xl shadow-2xl p-6 w-full max-w-6xl h-[90vh] flex flex-col animate-fadeIn">
<h2 className="text-2xl font-semibold mb-4 text-center text-gray-800">
{openTable.stage} {openTable.data.length} Properties
</h2>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-md transition-opacity">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
className="bg-white rounded-2xl shadow-2xl p-8 w-full max-w-6xl h-[90vh] flex flex-col border border-brandblue/10"
>
<div className="mb-6 border-b border-brandblue/10 pb-6">
<h2 className="text-2xl font-bold text-brandblue mb-3">
{openTable.stage}
</h2>
<p className="text-sm text-gray-600 mb-4">
Showing <span className="font-semibold text-brandblue">{openTable.data.length}</span> properties
</p>
<div className="flex-1 overflow-auto">
{/* Breakdown Stats */}
{openTable.breakdown && (
<div className="grid grid-cols-2 gap-3">
{Object.entries(openTable.breakdown).map(([category, items]) => (
<div key={category} className="bg-gradient-to-br from-brandlightblue/20 to-brandlightblue/10 rounded-lg p-3 border border-brandblue/10">
<p className="text-xs uppercase tracking-wide font-semibold text-brandblue/70 mb-1">
{category}
</p>
<p className="text-2xl font-bold text-brandblue">
{items.length}
</p>
<p className="text-xs text-gray-500 mt-1">
{((items.length / openTable.data.length) * 100).toFixed(0)}% of total
</p>
</div>
))}
</div>
)}
</div>
<div className="flex-1 overflow-auto rounded-lg border border-gray-100">
<TableViewer
data={openTable.data}
columns={openTable.columns}
@ -223,15 +246,15 @@ export default function LiveTracker({ deals }: ReportsProps) {
/>
</div>
<div className="mt-4 flex justify-center">
<div className="mt-6 flex justify-end gap-3">
<button
onClick={() => setOpenTable(null)}
className="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition"
className="px-6 py-2.5 bg-gradient-to-r from-brandblue to-brandmidblue text-white font-medium rounded-lg hover:shadow-md transition-all duration-200 hover:opacity-90"
>
Close
</button>
</div>
</div>
</motion.div>
</div>
)}
</div>
@ -254,30 +277,46 @@ function StatCard({
onClick: () => void;
accent?: "brandblue" | "red";
}) {
const accentColor =
accent === "red"
? "from-red-50 to-white text-red-600 hover:border-red-300"
: "from-brandlightblue/20 to-white text-brandblue hover:border-brandblue/40";
const accentConfig = {
brandblue: {
gradient: "from-brandlightblue/30 to-brandlightblue/10",
border: "border-brandblue/20",
text: "text-brandblue",
value: "text-brandblue",
hover: "hover:border-brandblue/40 hover:shadow-lg",
icon: "text-brandblue"
},
red: {
gradient: "from-red-50/50 to-red-50/20",
border: "border-red-200/50",
text: "text-red-600",
value: "text-red-700",
hover: "hover:border-red-300 hover:shadow-lg",
icon: "text-red-600"
}
};
const config = accentConfig[accent];
return (
<motion.button
onClick={onClick}
whileHover={{ scale: 1.02 }}
className={`group relative text-left border rounded-xl bg-gradient-to-br ${accentColor} transition-all duration-200 shadow-sm hover:shadow-md p-5`}
className={`group relative text-left border rounded-xl bg-gradient-to-br ${config.gradient} ${config.border} transition-all duration-300 shadow-sm ${config.hover} p-6`}
>
<div className="flex items-center justify-between">
<div>
<p className="text-xs uppercase text-gray-500 mb-1">{title}</p>
<p className="text-3xl font-bold text-gray-800 group-hover:text-inherit">
<p className={`text-xs uppercase tracking-wide font-semibold ${config.text} opacity-70 mb-3`}>{title}</p>
<p className={`text-3xl font-bold ${config.value} group-hover:opacity-90 transition-opacity`}>
{value}
{subtitle && (
<span className="text-base font-medium text-gray-500 ml-1">
<span className="text-base font-medium text-gray-600 ml-2">
{subtitle}
</span>
)}
</p>
</div>
<Icon className="h-6 w-6 opacity-50 group-hover:opacity-100 transition" />
<Icon className={`h-8 w-8 ${config.icon} opacity-40 group-hover:opacity-70 transition-all duration-300`} />
</div>
</motion.button>
);

View file

@ -66,13 +66,13 @@ export default function SurveyedPieChart({
}
return (
<Card className="flex flex-col items-center p-6 pt-10 pb-8 bg-white">
<Card className="flex flex-col items-center p-8 bg-gradient-to-br from-white to-brandlightblue/5 border border-brandblue/10">
{/* Header */}
<div className="text-center mb-4">
<Title className="text-gray-800 text-[15px] font-semibold tracking-tight">
<div className="text-center mb-8 pb-6 border-b border-brandblue/10 w-full">
<Title className="text-brandblue text-[16px] font-bold tracking-tight mb-2">
Survey Performance
</Title>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-gray-500">
Click a segment or label to view filtered properties
</p>
</div>
@ -94,14 +94,14 @@ export default function SurveyedPieChart({
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"
className="bg-white/90 backdrop-blur-md px-4 py-3 rounded-lg shadow-lg
border border-brandblue/20 text-gray-800 text-sm font-medium"
>
<div className="flex flex-col items-center">
<span className="text-[0.95rem] font-semibold text-gray-900">
<span className="text-[0.95rem] font-bold text-brandblue">
{name}
</span>
<span className="opacity-70">{amount.toLocaleString()}</span>
<span className="text-gray-600 text-xs mt-1">{amount.toLocaleString()}</span>
</div>
</div>
);
@ -109,7 +109,7 @@ export default function SurveyedPieChart({
/>
{data.length > 0 && (
<div className="absolute text-center">
<span className="text-3xl font-semibold text-gray-800">
<span className="text-4xl font-bold text-brandblue">
{data.reduce((a, b) => a + b.amount, 0)}
</span>
</div>
@ -117,41 +117,41 @@ export default function SurveyedPieChart({
</div>
{/* Legend (Clean Grid Layout) */}
<div className="mt-8 flex flex-wrap justify-center gap-x-6 gap-y-3 max-w-[90%]">
<div className="mt-10 flex flex-wrap justify-center gap-x-6 gap-y-3 max-w-[95%] border-t border-brandblue/10 pt-8">
{data.map((item, idx) => (
<div
<button
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"
className="relative flex items-center space-x-2 text-sm text-gray-700 hover:text-brandblue cursor-pointer transition-colors px-3 py-2 rounded-lg hover:bg-brandlightblue/20"
>
<span
className={`inline-block w-3.5 h-3.5 rounded-full bg-${colors[idx]} border border-gray-300 flex-shrink-0`}
className={`inline-block w-3 h-3 rounded-full bg-${colors[idx]} border-2 border-${colors[idx]}/40 flex-shrink-0 transition-all`}
/>
<span className="font-medium truncate max-w-[110px]">
<span className="font-medium truncate max-w-[100px]">
{item.name}
</span>
<span className="text-xs text-gray-500 ml-1 whitespace-nowrap">
<span className="text-xs text-gray-500 ml-1 whitespace-nowrap font-semibold">
{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
className="absolute -top-12 left-1/2 -translate-x-1/2 bg-white/95 backdrop-blur-md
px-4 py-3 rounded-lg shadow-lg border border-brandblue/20 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">
<span className="text-[0.95rem] font-bold text-brandblue">
{item.name}
</span>
<span className="opacity-70">{item.amount.toLocaleString()}</span>
<span className="text-gray-600 text-xs mt-1">{item.amount.toLocaleString()}</span>
</div>
</div>
)}
</div>
</button>
))}
</div>
</Card>

View file

@ -78,10 +78,10 @@ export default function TableViewer({
<button
key={idx}
onClick={() => handleDownload(url)}
className="flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-600 text-xs rounded hover:bg-blue-100 transition"
className="flex items-center gap-2 px-3 py-1.5 bg-gradient-to-r from-brandblue/10 to-brandmidblue/10 text-brandblue text-xs font-medium rounded-lg hover:from-brandblue/20 hover:to-brandmidblue/20 border border-brandblue/20 hover:border-brandblue/40 transition-all duration-300 active:scale-95"
>
<Download className="w-3 h-3" />
<span>Download Photos</span>
<Download className="w-3.5 h-3.5" />
<span>Download</span>
</button>
))}
</div>
@ -92,21 +92,21 @@ export default function TableViewer({
};
return (
<div className="overflow-x-auto border rounded-xl shadow-lg bg-white">
<div className="overflow-x-auto border border-brandblue/10 rounded-xl shadow-lg bg-white">
<table className="min-w-full text-sm border-collapse">
<thead className="bg-gray-100 sticky top-0">
<thead className="bg-gradient-to-r from-brandblue/5 to-brandmidblue/5 sticky top-0 border-b border-brandblue/10">
<tr>
{visibleColumns.map((col) => (
<th
key={col}
className="border-b p-3 text-left text-gray-700 font-semibold"
className="p-4 text-left font-bold text-brandblue"
>
<div className="flex flex-col gap-1">
<span>{columnLabels?.[col] || col}</span>
<div className="flex flex-col gap-2">
<span className="text-xs uppercase tracking-wide">{columnLabels?.[col] || col}</span>
<input
type="text"
placeholder="Search..."
className="p-1 border border-gray-300 rounded text-xs focus:ring-1 focus:ring-blue-400 outline-none"
className="p-2 border border-brandblue/20 rounded-lg text-xs focus:ring-2 focus:ring-brandblue focus:border-brandblue outline-none bg-white hover:border-brandblue/40 transition-all"
onChange={(e) =>
setSearchTerms((prev) => ({
...prev,
@ -124,7 +124,7 @@ export default function TableViewer({
<tr>
<td
colSpan={visibleColumns.length}
className="text-center py-6 text-gray-400"
className="text-center py-8 text-gray-500 font-medium"
>
No results found
</td>
@ -133,10 +133,10 @@ export default function TableViewer({
filteredData.map((row, i) => (
<tr
key={i}
className="odd:bg-white even:bg-gray-50 hover:bg-blue-50 transition"
className="odd:bg-white even:bg-brandlightblue/3 hover:bg-brandlightblue/10 transition-colors border-b border-brandblue/5 last:border-b-0"
>
{visibleColumns.map((col) => (
<td key={col} className="border-b p-3 text-gray-700">
<td key={col} className="p-4 text-gray-800">
{renderCellContent(col, row[col])}
</td>
))}