mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
added the live project tracking and project proposal
This commit is contained in:
parent
25a32a5d1c
commit
87655f7d08
13 changed files with 454 additions and 273 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
BuildingOfficeIcon,
|
||||
ChartBarIcon,
|
||||
HomeModernIcon,
|
||||
RocketLaunchIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
NavigationMenu,
|
||||
|
|
@ -12,98 +13,90 @@ import {
|
|||
NavigationMenuList,
|
||||
} from "@/app/shadcn_components/ui/navigation-menu";
|
||||
import AddNewDropDown from "./AddNew";
|
||||
import { cva } from "class-variance-authority";
|
||||
import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ScenarioSelect } from "@/app/db/schema/recommendations";
|
||||
import { useState } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ToolbarProps {
|
||||
portfolioId: string;
|
||||
scenarios: ScenarioSelect[];
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"bg-gray-50 cursor-pointer group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-200 hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-gray-200 text-gray-900"
|
||||
);
|
||||
|
||||
export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
||||
const router = useRouter();
|
||||
|
||||
function handleClickSettings() {
|
||||
router.push(`/portfolio/${portfolioId}/settings`);
|
||||
}
|
||||
|
||||
function handleClickPortfolio() {
|
||||
router.push(`/portfolio/${portfolioId}`);
|
||||
}
|
||||
|
||||
function handleClickSummary() {
|
||||
router.push(`/portfolio/${portfolioId}/summary`);
|
||||
}
|
||||
|
||||
// function handleClickMeasures() {
|
||||
// router.push(`/portfolio/${portfolioId}/measures`);
|
||||
// }
|
||||
function handleClickDecentHomes() {
|
||||
router.push(`/portfolio/${portfolioId}/decent-homes`);
|
||||
}
|
||||
|
||||
function handleClickProgressReport() {
|
||||
router.push(`/portfolio/${portfolioId}/live-projects`);
|
||||
}
|
||||
|
||||
const pathname = usePathname();
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [isRemoteAssessmentOpen, setIsRemoteAssessmentOpen] = useState(false);
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
label: "Portfolio",
|
||||
icon: BuildingOfficeIcon,
|
||||
match: (p: string) => p === `/portfolio/${portfolioId}`,
|
||||
href: `/portfolio/${portfolioId}`,
|
||||
},
|
||||
{
|
||||
label: "Retrofit Summary",
|
||||
icon: ChartBarIcon,
|
||||
match: (p: string) => p.startsWith(`/portfolio/${portfolioId}/summary`),
|
||||
href: `/portfolio/${portfolioId}/summary`,
|
||||
},
|
||||
{
|
||||
label: "Decent Homes",
|
||||
icon: HomeModernIcon,
|
||||
match: (p: string) =>
|
||||
p.startsWith(`/portfolio/${portfolioId}/decent-homes`),
|
||||
href: `/portfolio/${portfolioId}/decent-homes`,
|
||||
},
|
||||
{
|
||||
label: "Your Projects",
|
||||
icon: RocketLaunchIcon,
|
||||
match: (p: string) =>
|
||||
p.startsWith(`/portfolio/${portfolioId}/your-projects`),
|
||||
href: `/portfolio/${portfolioId}/your-projects/proposal`,
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
icon: Cog6ToothIcon,
|
||||
match: (p: string) => p.startsWith(`/portfolio/${portfolioId}/settings`),
|
||||
href: `/portfolio/${portfolioId}/settings`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem
|
||||
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
|
||||
onClick={handleClickPortfolio}
|
||||
>
|
||||
<BuildingOfficeIcon className="h-4 w-4 mr-2" />
|
||||
Portfolio
|
||||
</NavigationMenuItem>
|
||||
{navItems.map(({ label, icon: Icon, href, match }) => {
|
||||
const isActive = match(pathname);
|
||||
|
||||
<NavigationMenuItem
|
||||
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
|
||||
onClick={handleClickSummary}
|
||||
>
|
||||
<ChartBarIcon className="h-4 w-4 mr-2" />
|
||||
Retrofit Summary
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem
|
||||
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
|
||||
onClick={handleClickDecentHomes}
|
||||
>
|
||||
<HomeModernIcon className="h-4 w-4 mr-2" />
|
||||
Decent Homes
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* <NavigationMenuItem
|
||||
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
|
||||
onClick={handleClickMeasures}
|
||||
>
|
||||
<WrenchScrewdriverIcon className="h-4 w-4 mr-2" />
|
||||
Measures
|
||||
</NavigationMenuItem> */}
|
||||
<NavigationMenuItem
|
||||
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
|
||||
onClick={handleClickProgressReport}
|
||||
>
|
||||
<ChartBarIcon className="h-4 w-4 mr-2" />
|
||||
Live Projects
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem
|
||||
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
|
||||
onClick={handleClickSettings}
|
||||
>
|
||||
<Cog6ToothIcon className="h-4 w-4 mr-2" />
|
||||
Settings
|
||||
</NavigationMenuItem>
|
||||
return (
|
||||
<NavigationMenuItem key={label} className="mx-1">
|
||||
<button
|
||||
onClick={() => router.push(href)}
|
||||
className={cn(
|
||||
"relative flex items-center justify-center rounded-md text-sm font-medium transition-all duration-300 p-[3px]",
|
||||
isActive
|
||||
? "bg-gradient-to-r from-brandblue via-brandbrown to-brandblue"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-md px-4 py-2 transition-colors duration-300",
|
||||
isActive
|
||||
? "bg-white text-brandblue shadow-sm"
|
||||
: "bg-gray-50 text-gray-800 hover:text-brandblue hover:bg-midblue hover:text-gray-100"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
{label}
|
||||
</div>
|
||||
</button>
|
||||
</NavigationMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
<AddNewDropDown
|
||||
portfolioId={portfolioId}
|
||||
|
|
@ -113,6 +106,7 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
setIsRemoteAssessmentOpen={setIsRemoteAssessmentOpen}
|
||||
/>
|
||||
</NavigationMenuList>
|
||||
|
||||
<UploadCsvModal
|
||||
isOpen={modalIsOpen}
|
||||
setIsOpen={setModalIsOpen}
|
||||
|
|
|
|||
|
|
@ -1,156 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DealStageChart } from "./DealStageChart";
|
||||
import SurveyedPieChart from "./SurveyedResultsPieChart";
|
||||
import TableViewer from "./TableViewer";
|
||||
|
||||
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";
|
||||
(acc[project] ||= []).push(deal);
|
||||
return acc;
|
||||
}, {} as Record<string, any[]>);
|
||||
|
||||
const [openTable, setOpenTable] = useState<{ stage: string; data: any[] } | null>(null);
|
||||
const projectCodes = Object.keys(groupedDeals);
|
||||
const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]);
|
||||
const currentDeals = groupedDeals[currentProjectCode];
|
||||
const totalProperties = deals.length;
|
||||
const majorConditionDeals = deals.filter(d => d.dealstage === MAJOR_CONDITION_STAGE_ID);
|
||||
const majorIssues = majorConditionDeals.length;
|
||||
const majorPercent = ((majorIssues / totalProperties) * 100).toFixed(1);
|
||||
|
||||
const handleOpenTable = (stage: string, filteredDeals: any[]) => {
|
||||
setOpenTable({ stage, data: filteredDeals });
|
||||
};
|
||||
|
||||
if (!deals?.length) {
|
||||
return (
|
||||
<div className="p-6 text-center text-gray-500 border rounded-2xl shadow-sm bg-white">
|
||||
No deal data available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-10">
|
||||
{/* 🌍 Global Portfolio Overview */}
|
||||
<div className="border rounded-2xl bg-gradient-to-br from-gray-50 to-white shadow-lg p-8 space-y-6">
|
||||
<h2 className="text-center text-xl font-semibold text-gray-800 tracking-tight">
|
||||
🌍 Global Portfolio Overview
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-center">
|
||||
{/* Total */}
|
||||
<button
|
||||
onClick={() => handleOpenTable("All Properties", deals)}
|
||||
className="group transition rounded-xl border bg-white shadow-sm hover:shadow-md hover:border-blue-400 p-5"
|
||||
>
|
||||
<p className="text-sm text-gray-500 uppercase tracking-wide">Total Properties</p>
|
||||
<p className="text-3xl font-bold text-gray-800 group-hover:text-blue-600">
|
||||
{totalProperties}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* Major Issues */}
|
||||
<button
|
||||
onClick={() => handleOpenTable("Major Condition Issues", majorConditionDeals)}
|
||||
className="group transition rounded-xl border bg-white shadow-sm hover:shadow-md hover:border-red-400 p-5"
|
||||
>
|
||||
<p className="text-sm text-gray-500 uppercase tracking-wide">Major Condition Issues</p>
|
||||
<p className="text-3xl font-bold text-red-600 group-hover:text-red-700">
|
||||
{majorIssues}{" "}
|
||||
<span className="text-gray-500 text-base font-medium">
|
||||
({majorPercent}%)
|
||||
</span>
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* Project Selector */}
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<label
|
||||
htmlFor="projectSelect"
|
||||
className="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
Select Project
|
||||
</label>
|
||||
<div className="relative w-60">
|
||||
<select
|
||||
id="projectSelect"
|
||||
value={currentProjectCode}
|
||||
onChange={(e) => setCurrentProjectCode(e.target.value)}
|
||||
className="w-full appearance-none px-4 py-2 pr-10 border rounded-lg shadow-sm bg-white text-gray-800 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
{projectCodes.map((code) => (
|
||||
<option key={code} value={code}>
|
||||
{code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute right-3 top-2.5 text-gray-500 pointer-events-none">▼</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 📊 Project Insights */}
|
||||
<div className="border rounded-2xl bg-gradient-to-br from-gray-50 to-white shadow-lg p-8 space-y-6">
|
||||
<h2 className="text-center text-xl font-semibold text-gray-800 tracking-tight">
|
||||
📊 Project-Level Insights
|
||||
</h2>
|
||||
<p className="text-center text-gray-500 text-sm">
|
||||
Showing data for <span className="font-medium text-gray-700">{currentProjectCode}</span>
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="border rounded-xl p-5 shadow-md bg-white hover:shadow-lg transition">
|
||||
<DealStageChart deals={currentDeals} onOpenTable={handleOpenTable} />
|
||||
</div>
|
||||
<div className="border rounded-xl p-5 shadow-md bg-white hover:shadow-lg transition">
|
||||
<SurveyedPieChart deals={currentDeals} onOpenTable={handleOpenTable} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🔹 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="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",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button
|
||||
onClick={() => setOpenTable(null)}
|
||||
className="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function TabLink({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const isActive = pathname === href;
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
if (isActive) return;
|
||||
startTransition(() => router.push(href)); // triggers route change
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={isPending}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-medium border-b-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
isActive
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground hover:border-primary"
|
||||
)}
|
||||
>
|
||||
{isPending ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="animate-spin h-3 w-3 border-2 border-primary border-t-transparent rounded-full" />
|
||||
{children}
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { TabLink } from "./TabLInk";
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="flex justify-center border-b mb-6 max-w-8xl mx-auto">
|
||||
<TabLink href={`/portfolio/${slug}/your-projects/proposal`}>
|
||||
Proposal
|
||||
</TabLink>
|
||||
<TabLink href={`/portfolio/${slug}/your-projects/live`}>
|
||||
Live Reporting
|
||||
</TabLink>
|
||||
</div>
|
||||
|
||||
<div>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,12 +4,12 @@ import { useMemo } from "react";
|
|||
import { BarList, Card, Title } from "@tremor/react";
|
||||
|
||||
const STAGE_ORDER = [
|
||||
"Initial Planning",
|
||||
"Booking Team to contact Tenant",
|
||||
"Survey in Progress",
|
||||
"Initial planning",
|
||||
"Booking team to contact tenant",
|
||||
"Survey in progress",
|
||||
"Not viable",
|
||||
"Needs HA Support",
|
||||
"Coordination + Design",
|
||||
"Needs support",
|
||||
"Coordination + design",
|
||||
"Ready to be installed",
|
||||
];
|
||||
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
"use client";
|
||||
|
||||
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 { Home, AlertTriangle, BarChart3 } 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) {
|
||||
const groupedDeals = deals.reduce(
|
||||
(acc, deal) => {
|
||||
const project = deal.projectCode || "Unknown Project";
|
||||
(acc[project] ||= []).push(deal);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any[]>
|
||||
);
|
||||
|
||||
const [openTable, setOpenTable] = useState<{
|
||||
stage: string;
|
||||
data: any[];
|
||||
} | null>(null);
|
||||
const projectCodes = Object.keys(groupedDeals);
|
||||
const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]);
|
||||
const currentDeals = groupedDeals[currentProjectCode];
|
||||
const totalProperties = deals.length;
|
||||
const majorConditionDeals = deals.filter(
|
||||
(d) => d.dealstage === MAJOR_CONDITION_STAGE_ID
|
||||
);
|
||||
const majorIssues = majorConditionDeals.length;
|
||||
const majorPercent = ((majorIssues / totalProperties) * 100).toFixed(1);
|
||||
|
||||
const handleOpenTable = (stage: string, filteredDeals: any[]) => {
|
||||
setOpenTable({ stage, data: filteredDeals });
|
||||
};
|
||||
|
||||
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">
|
||||
<CardContent>
|
||||
<p className="text-gray-500 text-sm">No deal data available.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
{/* 🌍 Global Overview */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{/* Total Properties */}
|
||||
<StatCard
|
||||
icon={Home}
|
||||
title="Total Properties"
|
||||
value={totalProperties}
|
||||
onClick={() => handleOpenTable("All Properties", deals)}
|
||||
accent="brandblue"
|
||||
/>
|
||||
|
||||
{/* Major Issues */}
|
||||
<StatCard
|
||||
icon={AlertTriangle}
|
||||
title="Major Condition Issues"
|
||||
value={`${majorIssues} `}
|
||||
subtitle={`(${majorPercent}%)`}
|
||||
onClick={() =>
|
||||
handleOpenTable("Major Condition Issues", majorConditionDeals)
|
||||
}
|
||||
accent="red"
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
className="border rounded-xl p-5 bg-white shadow-sm hover:shadow-md transition"
|
||||
>
|
||||
<DealStageChart
|
||||
deals={currentDeals}
|
||||
onOpenTable={handleOpenTable}
|
||||
/>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 🔹 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="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",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button
|
||||
onClick={() => setOpenTable(null)}
|
||||
className="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 🔸Small stat card to match DashboardSummary visuals */
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
onClick,
|
||||
accent = "brandblue",
|
||||
}: {
|
||||
icon: any;
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
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";
|
||||
|
||||
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`}
|
||||
>
|
||||
<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">
|
||||
{value}
|
||||
{subtitle && (
|
||||
<span className="text-base font-medium text-gray-500 ml-1">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Icon className="h-6 w-6 opacity-50 group-hover:opacity-100 transition" />
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
|
@ -60,12 +60,12 @@ export default function SurveyedPieChart({
|
|||
index="name"
|
||||
valueFormatter={(n) => `${n.toLocaleString()}`}
|
||||
colors={[
|
||||
"sky",
|
||||
"cyan",
|
||||
"blue",
|
||||
"indigo",
|
||||
"violet",
|
||||
"slate",
|
||||
"#2d348f",
|
||||
"#14163d",
|
||||
"#3943b7",
|
||||
"#5d6be0",
|
||||
"black",
|
||||
"#eff6fc",
|
||||
"lightBlue",
|
||||
"navy",
|
||||
"azure",
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { getServerSession } from "next-auth";
|
||||
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { redirect } from "next/navigation";
|
||||
import { surveyDB } from "../../../../db/surveyDB/connection";
|
||||
import { hubspotDealData } from "../../../../db/schema/crm/hubspot_deal_table";
|
||||
import { surveyDB } from "../../../../../db/surveyDB/connection";
|
||||
import { hubspotDealData } from "../../../../../db/schema/crm/hubspot_deal_table";
|
||||
import { hubspotCompanyData } from "@/app/db/schema/crm/hubspot_company_table";
|
||||
import { eq } from "drizzle-orm";
|
||||
import LiveTracker from "./Report";
|
||||
|
||||
export default async function Demo(props: {
|
||||
export default async function LiveReportingPage(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug: portfolioId } = await props.params;
|
||||
const user = await getServerSession(AuthOptions);
|
||||
|
||||
if (!user?.user) {
|
||||
|
|
@ -17,8 +18,6 @@ export default async function Demo(props: {
|
|||
redirect("/");
|
||||
}
|
||||
|
||||
const { slug: portfolioId } = await props.params;
|
||||
|
||||
// 🏢 Fetch the company
|
||||
const [company] = await surveyDB
|
||||
.select()
|
||||
|
|
@ -52,22 +51,18 @@ export default async function Demo(props: {
|
|||
}
|
||||
|
||||
return (
|
||||
<main className="relative min-h-screen overflow-hidden">
|
||||
{/* 🌊 Domna-inspired layered background */}
|
||||
{/* <div className="absolute inset-0 -z-10 bg-[linear-gradient(135deg,#14163d_0%,#2d348f_45%,#3943b7_70%,#eff6fc_100%)]" /> */}
|
||||
|
||||
{/* ✨ Subtle translucent grid texture */}
|
||||
<div className="absolute inset-0 -z-0 opacity-10 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.7)_1px,transparent_0)] bg-[length:40px_40px]" />
|
||||
|
||||
{/* 💡 Optional soft light glow at top */}
|
||||
<div className="absolute top-0 inset-x-0 h-[40vh] bg-gradient-to-b from-white/20 via-transparent to-transparent opacity-30" />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 p-6 md:p-10">
|
||||
<div className="max-w-7xl mx-auto animate-fadeIn">
|
||||
<LiveTracker deals={deals} />
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto px-6 pb-10 space-y-4">
|
||||
<div className="mb-6">
|
||||
<header className="text-3xl font-semibold text-brandblue">
|
||||
Live Projects
|
||||
</header>
|
||||
<p className="text-sm text-gray-500">
|
||||
Check in on your projects' progress with real-time data updates.
|
||||
</p>
|
||||
<div className="h-px bg-gray-200 mt-2" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<LiveTracker deals={deals} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -120,9 +120,7 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-1">
|
||||
Total client contribution
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-1">Total investment</p>
|
||||
<p className="text-2xl font-semibold text-brandbrown">
|
||||
£{formatNumber(selectedData?.totalClientContribution || 0)}
|
||||
</p>
|
||||
|
|
@ -3,10 +3,22 @@
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import { formatNumber, getEpcColorClass, sapToEpc } from "@/app/utils";
|
||||
import StatusBadge from "@/app/components/StatusBadge";
|
||||
import { PlanWithTotals } from "./utils";
|
||||
|
||||
const EpcLetterBubble = ({ letter }: { letter: string }) => {
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex items-center justify-center w-6 h-6 rounded-full ${getEpcColorClass(
|
||||
letter
|
||||
)} text-white text-m font-bold shadow-outline-black`}
|
||||
>
|
||||
{letter}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
||||
{
|
||||
accessorKey: "landlordPropertyId",
|
||||
|
|
@ -138,4 +150,32 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
),
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
{
|
||||
accessorKey: "currentEpc",
|
||||
header: () => <div className="flex justify-center">Current EPC Rating</div>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-gray-700 font-medium flex justify-center">
|
||||
{<EpcLetterBubble letter={row.original.currentEpcRating || ""} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "targetEpc",
|
||||
header: () => <div className="flex justify-center">Expected EPC</div>,
|
||||
cell: ({ row }) => {
|
||||
const currentSapPoints = row.original.currentSapPoints || 0;
|
||||
|
||||
const expectedSapPoints = row.original.totalRecommendationSapPoints || 0;
|
||||
|
||||
const expectedEpc = sapToEpc(currentSapPoints + expectedSapPoints);
|
||||
|
||||
return (
|
||||
<div className="text-gray-700 font-medium flex justify-center">
|
||||
{<EpcLetterBubble letter={expectedEpc || ""} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -3,16 +3,14 @@ import { getPlansWithTotals } from "./utils";
|
|||
import DataTable from "@/app/portfolio/[slug]/components/propertyTable";
|
||||
import { planColumns } from "./ProposalColumns";
|
||||
|
||||
export default async function YourProjectsPage({
|
||||
params,
|
||||
}: {
|
||||
export default async function ProjectProposalPage(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug: portfolioId } = await params;
|
||||
const { slug: portfolioId } = await props.params;
|
||||
const latestPlans = await getPlansWithTotals(portfolioId);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-6 py-10 space-y-4">
|
||||
<div className="max-w-7xl mx-auto px-6 pb-10 space-y-4">
|
||||
<div className="mb-6">
|
||||
<header className="text-3xl font-semibold text-brandblue">
|
||||
Project Overview
|
||||
|
|
@ -12,6 +12,8 @@ export interface PlanWithTotals extends Record<string, unknown> {
|
|||
landlordPropertyId: string | null;
|
||||
address: string | null;
|
||||
postcode: string | null;
|
||||
currentSapPoints: number | null;
|
||||
currentEpcRating: string | null;
|
||||
fundingScheme: string | null;
|
||||
totalFunding: number | null;
|
||||
totalUplift: number | null;
|
||||
|
|
@ -20,6 +22,7 @@ export interface PlanWithTotals extends Record<string, unknown> {
|
|||
totalRecommendationCost?: number | null;
|
||||
surveyCost?: number;
|
||||
clientContribution?: number;
|
||||
totalRecommendationSapPoints: number | null;
|
||||
}
|
||||
|
||||
export async function getPlansWithTotals(
|
||||
|
|
@ -35,12 +38,15 @@ export async function getPlansWithTotals(
|
|||
p.landlord_property_id AS "landlordPropertyId",
|
||||
p.address AS "address",
|
||||
p.postcode AS "postcode",
|
||||
p.current_sap_points AS "currentSapPoints",
|
||||
p.current_epc_rating AS "currentEpcRating",
|
||||
fp.scheme AS "fundingScheme",
|
||||
COALESCE(fp.project_funding, 0) AS "totalFunding",
|
||||
COALESCE(fp.total_uplift, 0) AS "totalUplift",
|
||||
COALESCE(SUM(r.co2_equivalent_savings), 0) AS "totalCarbonSavings",
|
||||
COALESCE(SUM(r.energy_cost_savings), 0) AS "totalBillSavings",
|
||||
COALESCE(SUM(r.estimated_cost), 0) AS "totalRecommendationCost"
|
||||
COALESCE(SUM(r.estimated_cost), 0) AS "totalRecommendationCost",
|
||||
COALESCE(SUM(r.sap_points), 0) AS "totalRecommendationSapPoints"
|
||||
FROM plan pl
|
||||
INNER JOIN property p
|
||||
ON p.id = pl.property_id
|
||||
|
|
@ -68,6 +74,8 @@ export async function getPlansWithTotals(
|
|||
p.landlord_property_id,
|
||||
p.address,
|
||||
p.postcode,
|
||||
p.current_sap_points,
|
||||
p.current_epc_rating,
|
||||
fp.scheme,
|
||||
fp.project_funding,
|
||||
fp.total_uplift
|
||||
Loading…
Add table
Reference in a new issue