diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index 7fcae07..a723b14 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -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 ( - - - Portfolio - + {navItems.map(({ label, icon: Icon, href, match }) => { + const isActive = match(pathname); - - - Retrofit Summary - - - - - Decent Homes - - - {/* - - Measures - */} - - - Live Projects - - - - Settings - + return ( + + + + ); + })} + []; -} - -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); - - 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 ( -
- No deal data available. -
- ); - } - - return ( -
- {/* 🌍 Global Portfolio Overview */} -
-

- 🌍 Global Portfolio Overview -

- -
- {/* Total */} - - - {/* Major Issues */} - - - {/* Project Selector */} -
- -
- -
β–Ό
-
-
-
-
- - {/* πŸ“Š Project Insights */} -
-

- πŸ“Š Project-Level Insights -

-

- Showing data for {currentProjectCode} -

- -
-
- -
-
- -
-
-
- - {/* πŸ”Ή Modal */} - {openTable && ( -
-
-

- {openTable.stage} β€” {openTable.data.length} Properties -

- -
- -
- -
- -
-
-
- )} -
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/TabLInk.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/TabLInk.tsx new file mode 100644 index 0000000..8476159 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/TabLInk.tsx @@ -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 ( + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx new file mode 100644 index 0000000..7fb552f --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx @@ -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 ( +
+
+ + Proposal + + + Live Reporting + +
+ +
{children}
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/live-projects/DealStageChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx similarity index 96% rename from src/app/portfolio/[slug]/(portfolio)/live-projects/DealStageChart.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx index 4aec8d7..1a388d7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/live-projects/DealStageChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DealStageChart.tsx @@ -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", ]; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx new file mode 100644 index 0000000..30b0fb1 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/Report.tsx @@ -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[]; +} + +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 + ); + + 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 ( + + +

No deal data available.

+
+
+ ); + } + + return ( +
+ {/* 🌍 Global Overview */} +
+ {/* Total Properties */} + handleOpenTable("All Properties", deals)} + accent="brandblue" + /> + + {/* Major Issues */} + + handleOpenTable("Major Condition Issues", majorConditionDeals) + } + accent="red" + /> + + {/* Project Selector */} + + + +

+ Select Project +

+
+
+ +
+ +
+ β–Ό +
+
+
+
+
+ + {/* πŸ“Š Project Insights */} + + + + Project-Level Insights β€” {currentProjectCode} + + + + + + + + + + + + + + + {/* πŸ”Ή Table Modal */} + {openTable && ( +
+
+

+ {openTable.stage} β€” {openTable.data.length} Properties +

+ +
+ +
+ +
+ +
+
+
+ )} +
+ ); +} + +/** πŸ”Έ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 ( + +
+
+

{title}

+

+ {value} + {subtitle && ( + + {subtitle} + + )} +

+
+ +
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/live-projects/SurveyedResultsPieChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx similarity index 93% rename from src/app/portfolio/[slug]/(portfolio)/live-projects/SurveyedResultsPieChart.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx index 4a3e168..d31bcd5 100644 --- a/src/app/portfolio/[slug]/(portfolio)/live-projects/SurveyedResultsPieChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx @@ -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", diff --git a/src/app/portfolio/[slug]/(portfolio)/live-projects/TableViewer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx similarity index 100% rename from src/app/portfolio/[slug]/(portfolio)/live-projects/TableViewer.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/live-projects/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx similarity index 61% rename from src/app/portfolio/[slug]/(portfolio)/live-projects/page.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx index d5e4e7c..92b1542 100644 --- a/src/app/portfolio/[slug]/(portfolio)/live-projects/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -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 ( -
- {/* 🌊 Domna-inspired layered background */} - {/*
*/} - - {/* ✨ Subtle translucent grid texture */} -
- - {/* πŸ’‘ Optional soft light glow at top */} -
- - {/* Main content */} -
-
- -
+
+
+
+ Live Projects +
+

+ Check in on your projects' progress with real-time data updates. +

+
-
+ + + ); } diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx similarity index 98% rename from src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx index 7dbffd1..faa4b2d 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx @@ -120,9 +120,7 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
-

- Total client contribution -

+

Total investment

Β£{formatNumber(selectedData?.totalClientContribution || 0)}

diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProposalColumns.tsx similarity index 76% rename from src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProposalColumns.tsx index b6949a7..9412441 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProposalColumns.tsx @@ -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 ( +
+ {letter} +
+ ); +}; + export const planColumns: ColumnDef[] = [ { accessorKey: "landlordPropertyId", @@ -138,4 +150,32 @@ export const planColumns: ColumnDef[] = [ ), sortingFn: "alphanumeric", }, + { + accessorKey: "currentEpc", + header: () =>
Current EPC Rating
, + cell: ({ row }) => { + return ( +
+ {} +
+ ); + }, + }, + { + accessorKey: "targetEpc", + header: () =>
Expected EPC
, + cell: ({ row }) => { + const currentSapPoints = row.original.currentSapPoints || 0; + + const expectedSapPoints = row.original.totalRecommendationSapPoints || 0; + + const expectedEpc = sapToEpc(currentSapPoints + expectedSapPoints); + + return ( +
+ {} +
+ ); + }, + }, ]; diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/page.tsx similarity index 85% rename from src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/page.tsx index cef20f0..f0ee868 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/page.tsx @@ -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 ( -
+
Project Overview diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/utils.ts similarity index 89% rename from src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts rename to src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/utils.ts index 70b1cb5..db310d0 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/utils.ts @@ -12,6 +12,8 @@ export interface PlanWithTotals extends Record { 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 { 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