From 24527703be03853adb05b527afa6f732b78c5de4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 31 Jul 2024 11:27:01 +0100 Subject: [PATCH] Added scenarios mechanism for summary page --- .../[scenarioId]}/route.ts | 66 +++---- .../summary/SelectComparisonModal.tsx | 20 +-- .../portfolio/summary/SummaryTable.tsx | 45 ++--- .../[slug]/(portfolio)/summary/page.tsx | 18 +- src/app/portfolio/[slug]/utils.ts | 167 ++++++++++-------- tsconfig.json | 4 +- 6 files changed, 181 insertions(+), 139 deletions(-) rename src/app/api/portfolio/{summary/[portfolioId] => scenario/[scenarioId]}/route.ts (59%) diff --git a/src/app/api/portfolio/summary/[portfolioId]/route.ts b/src/app/api/portfolio/scenario/[scenarioId]/route.ts similarity index 59% rename from src/app/api/portfolio/summary/[portfolioId]/route.ts rename to src/app/api/portfolio/scenario/[scenarioId]/route.ts index fae683e2..32de0baa 100644 --- a/src/app/api/portfolio/summary/[portfolioId]/route.ts +++ b/src/app/api/portfolio/scenario/[scenarioId]/route.ts @@ -1,46 +1,46 @@ -import { portfolio } from "@/app/db/schema/portfolio"; import { NextRequest, NextResponse } from "next/server"; import { db } from "@/app/db/db"; import { z } from "zod"; import { formatNumber } from "@/app/utils"; import { DataItem, ChartData } from "@/app/portfolio/[slug]/utils"; import { eq } from "drizzle-orm"; +import { scenario } from "@/app/db/schema/recommendations"; export async function GET( request: NextRequest, - { params }: { params: { portfolioId: string } } + { params }: { params: { scenarioId: string } } ) { - const portfolioId = params.portfolioId; + const scenarioId = params.scenarioId; const data = await db .select({ - name: portfolio.name, - cost: portfolio.cost, - epcBreakdownPostRetrofit: portfolio.epcBreakdownPostRetrofit, - numberOfProperties: portfolio.numberOfProperties, - nUnitsToRetrofit: portfolio.nUnitsToRetrofit, - co2PerUnitPostRetrofit: portfolio.co2PerUnitPostRetrofit, - energyBillPerUnitPostRetrofit: portfolio.energyBillPerUnitPostRetrofit, + name: scenario.name, + cost: scenario.cost, + epcBreakdownPostRetrofit: scenario.epcBreakdownPostRetrofit, + numberOfProperties: scenario.numberOfProperties, + nUnitsToRetrofit: scenario.nUnitsToRetrofit, + co2PerUnitPostRetrofit: scenario.co2PerUnitPostRetrofit, + energyBillPerUnitPostRetrofit: scenario.energyBillPerUnitPostRetrofit, energyConsumptionPerUnitPostRetrofit: - portfolio.energyConsumptionPerUnitPostRetrofit, - valuationImprovementPerUnit: portfolio.valuationImprovementPerUnit, - costPerUnit: portfolio.costPerUnit, - costPerCo2Saved: portfolio.costPerCo2Saved, - costPerSapPoint: portfolio.costPerSapPoint, - valuationReturnOnInvestment: portfolio.valuationReturnOnInvestment, + scenario.energyConsumptionPerUnitPostRetrofit, + valuationImprovementPerUnit: scenario.valuationImprovementPerUnit, + costPerUnit: scenario.costPerUnit, + costPerCo2Saved: scenario.costPerCo2Saved, + costPerSapPoint: scenario.costPerSapPoint, + valuationReturnOnInvestment: scenario.valuationReturnOnInvestment, }) - .from(portfolio) - .where(eq(portfolio.id, BigInt(portfolioId))); + .from(scenario) + .where(eq(scenario.id, BigInt(scenarioId))); if (data.length === 0) { - throw new Error("Portfolio not found"); + throw new Error("Scenario not found"); } if (data.length > 1) { - throw new Error("More than one portfolio found"); + throw new Error("More than one scenario found"); } - const portfolioName = data[0].name; + const scenarioName = data[0].name || "Default"; // Format the data we need for the overview const output: DataItem[] = [ @@ -48,7 +48,7 @@ export async function GET( title: "EPCs", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: JSON.parse( data[0].epcBreakdownPostRetrofit || "[]" ) as ChartData[], @@ -59,7 +59,7 @@ export async function GET( title: "# Units", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: String(data[0].numberOfProperties) || "", }, ], @@ -68,7 +68,7 @@ export async function GET( title: "# Units to retrofit", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: String(data[0].nUnitsToRetrofit) || "", }, ], @@ -77,7 +77,7 @@ export async function GET( title: "Co2/unit", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: data[0].co2PerUnitPostRetrofit || "", }, ], @@ -86,7 +86,7 @@ export async function GET( title: "Annual energy bill/unit", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: data[0].energyBillPerUnitPostRetrofit || "", }, ], @@ -95,7 +95,7 @@ export async function GET( title: "Annual Energy consumption (kWh)/unit", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: data[0].energyConsumptionPerUnitPostRetrofit || "", }, ], @@ -104,7 +104,7 @@ export async function GET( title: "Cost (£)", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: "£" + formatNumber(data[0].cost || 0), }, ], @@ -112,19 +112,19 @@ export async function GET( { title: "Cost (£)/unit", scenarios: [ - { scenarioName: portfolioName, data: data[0].costPerUnit || "" }, + { scenarioName: scenarioName, data: data[0].costPerUnit || "" }, ], }, { title: "£ per CO2 reduction", scenarios: [ - { scenarioName: portfolioName, data: data[0].costPerCo2Saved || "" }, + { scenarioName: scenarioName, data: data[0].costPerCo2Saved || "" }, ], }, { title: "£ per SAP point", scenarios: [ - { scenarioName: portfolioName, data: data[0].costPerSapPoint || "" }, + { scenarioName: scenarioName, data: data[0].costPerSapPoint || "" }, ], }, { @@ -132,7 +132,7 @@ export async function GET( "Potential valuation improvement/unit - highly indicative EPC effect*", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: data[0].valuationImprovementPerUnit || "", }, ], @@ -142,7 +142,7 @@ export async function GET( "Potential valuation return on investment - highly indicative EPC effect*", scenarios: [ { - scenarioName: portfolioName, + scenarioName: scenarioName, data: data[0].valuationReturnOnInvestment || "", }, ], diff --git a/src/app/components/portfolio/summary/SelectComparisonModal.tsx b/src/app/components/portfolio/summary/SelectComparisonModal.tsx index 4468d668..cab21995 100644 --- a/src/app/components/portfolio/summary/SelectComparisonModal.tsx +++ b/src/app/components/portfolio/summary/SelectComparisonModal.tsx @@ -4,18 +4,18 @@ import { Fragment, useState } from "react"; const SelectComparisonModal = ({ isOpen, setIsOpen, - userPortfolios, + scenarios, onAddColumn, }: { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; - userPortfolios: { name: string; id: bigint }[]; + scenarios: { name: string | null; id: bigint }[]; onAddColumn: (columnName: string) => void; }) => { - const [selectedPortfolio, setSelectedPortfolio] = useState(""); + const [selectedScenario, setSelectedScenario] = useState(""); const addColumn = () => { - onAddColumn(selectedPortfolio); + onAddColumn(selectedScenario); setIsOpen(false); }; @@ -65,15 +65,15 @@ const SelectComparisonModal = ({
diff --git a/src/app/components/portfolio/summary/SummaryTable.tsx b/src/app/components/portfolio/summary/SummaryTable.tsx index 8272200e..ef7b2e0b 100644 --- a/src/app/components/portfolio/summary/SummaryTable.tsx +++ b/src/app/components/portfolio/summary/SummaryTable.tsx @@ -20,12 +20,12 @@ import EpcBarChart from "./EpcBarChart"; import { DataItem, ChartData } from "@/app/portfolio/[slug]/utils"; import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"; -export async function getComparsionOverviewPortfolioData( - portfolioId: string +export async function getComparsionOverviewScenarioData( + scenarioId: string ): Promise> { - const url = process.env.URL || "" + `/api/portfolio/summary/${portfolioId}`; + const url = process.env.URL || "" + `/api/portfolio/scenario/${scenarioId}`; - // This is the client side counterpart to getOverviewPortfolioData + // This is the client side counterpart to getOverviewScenarioData const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" }, @@ -37,21 +37,21 @@ export async function getComparsionOverviewPortfolioData( return response.json(); } -interface UseFetchPortfolioSummaryProps { - portfolioId: string; // Assuming portfolioId can be null to handle cases before it's available +interface UseFetchScenarioSummaryProps { + scenarioId: string; // Assuming scenarioId can be null to handle cases before it's available onSuccess: (data: DataItem[]) => void; options?: Omit, "queryKey" | "queryFn">; } -export const useFetchPortfolioSummary = ({ - portfolioId, +export const useFetchScenarioSummary = ({ + scenarioId, onSuccess, -}: UseFetchPortfolioSummaryProps) => { +}: UseFetchScenarioSummaryProps) => { return useQuery({ - queryKey: ["portfolioSummary", portfolioId], - queryFn: () => getComparsionOverviewPortfolioData(portfolioId as string), // Ensure non-null assertion or handle potential null inside the function + queryKey: ["scenarioSummary", scenarioId], + queryFn: () => getComparsionOverviewScenarioData(scenarioId as string), // Ensure non-null assertion or handle potential null inside the function onSuccess, - enabled: portfolioId !== "no-id", // Ensure the query only runs when portfolioId is not null + enabled: scenarioId !== "no-id", // Ensure the query only runs when portfolioId is not null }); }; @@ -64,10 +64,10 @@ const shouldHighlightRow = (title: string) => { const SummaryTable = ({ data, - userPortfolios, + scenarios, }: { data: DataItem[]; - userPortfolios: { name: string; id: bigint }[]; + scenarios: { name: string | null; id: bigint }[]; }) => { // TODO: Right now a user can ad multiple comparisons of the same column, we should prevent this @@ -117,16 +117,15 @@ const SummaryTable = ({ const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedPortfolioId, setSelectedPortfolioId] = - useState("no-id"); + const [selectedScenarioId, setSelectedScenarioId] = useState("no-id"); - // Fetch and handle data when a new portfolio is selected + // Fetch and handle data when a new scenario is selected const { isLoading, error, data: fetchedData, - } = useFetchPortfolioSummary({ - portfolioId: selectedPortfolioId, + } = useFetchScenarioSummary({ + scenarioId: selectedScenarioId, onSuccess: (fetchedData: DataItem[]) => { const updatedData = managedData.map((item) => ({ ...item, @@ -171,15 +170,17 @@ const SummaryTable = ({
setSelectedPortfolioId(id)} + scenarios={scenarios} + onAddColumn={(id: string) => setSelectedScenarioId(id)} />
diff --git a/src/app/portfolio/[slug]/(portfolio)/summary/page.tsx b/src/app/portfolio/[slug]/(portfolio)/summary/page.tsx index e64a2904..c78a39d7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/summary/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/summary/page.tsx @@ -1,5 +1,8 @@ import SummaryTable from "@/app/components/portfolio/summary/SummaryTable"; -import { getUserPortfolios, getOverviewPortfolioData } from "../../utils"; +import { + getNonDefaultPortfolioScenarios, + getOverviewPortfolioData, +} from "../../utils"; export default async function PortfolioSummary({ params, @@ -11,14 +14,23 @@ export default async function PortfolioSummary({ // Retrieve all of the names and ids of the portfolios, attributed to this user // Get user id from the session - const userPortfolios = await getUserPortfolios(portfolioId); + const scenarios = await getNonDefaultPortfolioScenarios(portfolioId); return (

Summary

- +
* Valuation improvement is based on these studies from + + Rightmove + + , { // Keep just the columns we need for the overview - const data = await db + // We firstly attempt to retrieve the data from the scenarios table, starting with the default + // scenario. If the default scenario doesn't exist, we'll use the data from the + // portfolio + let data = await db .select({ - name: portfolio.name, - cost: portfolio.cost, - epcBreakdownPreRetrofit: portfolio.epcBreakdownPreRetrofit, - epcBreakdownPostRetrofit: portfolio.epcBreakdownPostRetrofit, - numberOfProperties: portfolio.numberOfProperties, - nUnitsToRetrofit: portfolio.nUnitsToRetrofit, - co2PerUnitPreRetrofit: portfolio.co2PerUnitPreRetrofit, - co2PerUnitPostRetrofit: portfolio.co2PerUnitPostRetrofit, - energyBillPerUnitPreRetrofit: portfolio.energyBillPerUnitPreRetrofit, - energyBillPerUnitPostRetrofit: portfolio.energyBillPerUnitPostRetrofit, + name: scenario.name, + cost: scenario.cost, + epcBreakdownPreRetrofit: scenario.epcBreakdownPreRetrofit, + epcBreakdownPostRetrofit: scenario.epcBreakdownPostRetrofit, + numberOfProperties: scenario.numberOfProperties, + nUnitsToRetrofit: scenario.nUnitsToRetrofit, + co2PerUnitPreRetrofit: scenario.co2PerUnitPreRetrofit, + co2PerUnitPostRetrofit: scenario.co2PerUnitPostRetrofit, + energyBillPerUnitPreRetrofit: scenario.energyBillPerUnitPreRetrofit, + energyBillPerUnitPostRetrofit: scenario.energyBillPerUnitPostRetrofit, energyConsumptionPerUnitPreRetrofit: - portfolio.energyConsumptionPerUnitPreRetrofit, + scenario.energyConsumptionPerUnitPreRetrofit, energyConsumptionPerUnitPostRetrofit: - portfolio.energyConsumptionPerUnitPostRetrofit, - valuationImprovementPerUnit: portfolio.valuationImprovementPerUnit, - costPerUnit: portfolio.costPerUnit, - costPerCo2Saved: portfolio.costPerCo2Saved, - costPerSapPoint: portfolio.costPerSapPoint, - valuationReturnOnInvestment: portfolio.valuationReturnOnInvestment, + scenario.energyConsumptionPerUnitPostRetrofit, + valuationImprovementPerUnit: scenario.valuationImprovementPerUnit, + costPerUnit: scenario.costPerUnit, + costPerCo2Saved: scenario.costPerCo2Saved, + costPerSapPoint: scenario.costPerSapPoint, + valuationReturnOnInvestment: scenario.valuationReturnOnInvestment, }) - .from(portfolio) - .where(eq(portfolio.id, BigInt(portfolioId))); + .from(scenario) + .where( + and( + eq(scenario.portfolioId, BigInt(portfolioId)), + eq(scenario.isDefault, true) + ) + ); if (data.length === 0) { - throw new Error("Portfolio not found"); + // If we have no data, we fetch it from the scenarios. This will facilitate this for older portfolios + data = await db + .select({ + name: portfolio.name, + cost: portfolio.cost, + epcBreakdownPreRetrofit: portfolio.epcBreakdownPreRetrofit, + epcBreakdownPostRetrofit: portfolio.epcBreakdownPostRetrofit, + numberOfProperties: portfolio.numberOfProperties, + nUnitsToRetrofit: portfolio.nUnitsToRetrofit, + co2PerUnitPreRetrofit: portfolio.co2PerUnitPreRetrofit, + co2PerUnitPostRetrofit: portfolio.co2PerUnitPostRetrofit, + energyBillPerUnitPreRetrofit: portfolio.energyBillPerUnitPreRetrofit, + energyBillPerUnitPostRetrofit: portfolio.energyBillPerUnitPostRetrofit, + energyConsumptionPerUnitPreRetrofit: + portfolio.energyConsumptionPerUnitPreRetrofit, + energyConsumptionPerUnitPostRetrofit: + portfolio.energyConsumptionPerUnitPostRetrofit, + valuationImprovementPerUnit: portfolio.valuationImprovementPerUnit, + costPerUnit: portfolio.costPerUnit, + costPerCo2Saved: portfolio.costPerCo2Saved, + costPerSapPoint: portfolio.costPerSapPoint, + valuationReturnOnInvestment: portfolio.valuationReturnOnInvestment, + }) + .from(portfolio) + .where(eq(portfolio.id, BigInt(portfolioId))); + + // If we still have no data, we throw and error + if (data.length === 0) { + throw new Error("Portfolio not found"); + } } if (data.length > 1) { @@ -118,7 +155,7 @@ export async function getOverviewPortfolioData( ) as ChartData[], }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: JSON.parse( data[0].epcBreakdownPostRetrofit || "[]" ) as ChartData[], @@ -133,7 +170,7 @@ export async function getOverviewPortfolioData( data: String(data[0].numberOfProperties) || "", }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: String(data[0].numberOfProperties) || "", }, ], @@ -143,7 +180,7 @@ export async function getOverviewPortfolioData( scenarios: [ { scenarioName: "Today", data: "-" }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: String(data[0].nUnitsToRetrofit) || "", }, ], @@ -153,7 +190,7 @@ export async function getOverviewPortfolioData( scenarios: [ { scenarioName: "Today", data: data[0].co2PerUnitPreRetrofit || "" }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: data[0].co2PerUnitPostRetrofit || "", }, ], @@ -166,7 +203,7 @@ export async function getOverviewPortfolioData( data: data[0].energyBillPerUnitPreRetrofit || "", }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: data[0].energyBillPerUnitPostRetrofit || "", }, ], @@ -179,7 +216,7 @@ export async function getOverviewPortfolioData( data: data[0].energyConsumptionPerUnitPreRetrofit || "", }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: data[0].energyConsumptionPerUnitPostRetrofit || "", }, ], @@ -189,7 +226,7 @@ export async function getOverviewPortfolioData( scenarios: [ { scenarioName: "Today", data: "" }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: "£" + formatNumber(data[0].cost || 0), }, ], @@ -198,21 +235,30 @@ export async function getOverviewPortfolioData( title: "Cost (£)/unit", scenarios: [ { scenarioName: "Today", data: "" }, - { scenarioName: portfolioName, data: data[0].costPerUnit || "" }, + { + scenarioName: portfolioName || "Default", + data: data[0].costPerUnit || "", + }, ], }, { title: "£ per CO2 reduction", scenarios: [ { scenarioName: "Today", data: "" }, - { scenarioName: portfolioName, data: data[0].costPerCo2Saved || "" }, + { + scenarioName: portfolioName || "Default", + data: data[0].costPerCo2Saved || "", + }, ], }, { title: "£ per SAP point", scenarios: [ { scenarioName: "Today", data: "" }, - { scenarioName: portfolioName, data: data[0].costPerSapPoint || "" }, + { + scenarioName: portfolioName || "Default", + data: data[0].costPerSapPoint || "", + }, ], }, { @@ -221,7 +267,7 @@ export async function getOverviewPortfolioData( scenarios: [ { scenarioName: "Today", data: "" }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: data[0].valuationImprovementPerUnit || "", }, ], @@ -232,7 +278,7 @@ export async function getOverviewPortfolioData( scenarios: [ { scenarioName: "Today", data: "" }, { - scenarioName: portfolioName, + scenarioName: portfolioName || "Default", data: data[0].valuationReturnOnInvestment || "", }, ], @@ -242,42 +288,25 @@ export async function getOverviewPortfolioData( return output; } -export async function getUserPortfolios( +export async function getNonDefaultPortfolioScenarios( portfolioId: string -): Promise<{ id: bigint; name: string }[]> { - // This function will retrieve all of the portfolio ids attributed to a user - // This can be done by querying the portfolioUsers table +): Promise<{ id: bigint; name: string | null }[]> { + // For a given portfolio Id, we want to retrieve all of the scenarios that are attributed to this portfolio + // We exclude the default scenario + const scenarios = await db + .select({ + id: scenario.id, + name: scenario.name, + }) + .from(scenario) + .where( + and( + eq(scenario.portfolioId, BigInt(portfolioId)), + eq(scenario.isDefault, false) + ) + ); - const portfolioUser = await db.query.portfolioUsers.findFirst({ - where: eq(portfolioUsers.portfolioId, BigInt(portfolioId)), - }); - - if (!portfolioUser) { - throw new Error("Portfolio not found"); - } - - const userId = portfolioUser?.userId; - - if (!userId) { - throw new Error("User not found"); - } - - // We now filter portfolioUsers by the userId - const data = await db.query.portfolioUsers.findMany({ - where: eq(portfolioUsers.userId, userId), - with: { - portfolio: { - // Make sure this matches the relation name defined in relations setup - columns: { - id: true, - name: true, - }, - }, - }, - }); - - // Extract the portfolio data from the nested object - return data.map((item) => item.portfolio); + return scenarios; } export async function getProperties( diff --git a/tsconfig.json b/tsconfig.json index 7f10c944..64681559 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "allowSyntheticDefaultImports": true, - // "target": "es5", - "target": "ESNext", + "target": "es5", + // "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,