From affd57ed5a2d788dee5ce7fa88087a84c28223d8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 15 Mar 2025 18:32:04 +0000 Subject: [PATCH] allow ventilation sap points to be negative --- .../building-passport/RecommendationCard.tsx | 3 +- src/app/components/portfolio/Toolbar.tsx | 13 + .../{plan => measures}/PlanTable.tsx | 0 .../{plan => measures}/PlanTableColumns.tsx | 0 src/app/db/schema/recommendations.ts | 11 +- .../(portfolio)/{plan => measures}/page.tsx | 10 +- .../settings/PortfolioSettings.tsx | 258 ++++++++++-------- src/app/portfolio/[slug]/utils.ts | 84 ++++-- 8 files changed, 235 insertions(+), 144 deletions(-) rename src/app/components/portfolio/{plan => measures}/PlanTable.tsx (100%) rename src/app/components/portfolio/{plan => measures}/PlanTableColumns.tsx (100%) rename src/app/portfolio/[slug]/(portfolio)/{plan => measures}/page.tsx (59%) diff --git a/src/app/components/building-passport/RecommendationCard.tsx b/src/app/components/building-passport/RecommendationCard.tsx index c492a37..4633e38 100644 --- a/src/app/components/building-passport/RecommendationCard.tsx +++ b/src/app/components/building-passport/RecommendationCard.tsx @@ -189,7 +189,8 @@ export default function RecommendationCard({ SAP Points: - {cardComponent.sapPoints < 0.1 + {cardComponent.sapPoints < 0.1 && + cardComponent.type !== "mechanical_ventilation" ? "Negligible" : cardComponent.sapPoints} diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index c12bbbf..35d7718 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -4,6 +4,7 @@ import { Cog6ToothIcon, BuildingOfficeIcon, ChartBarIcon, + WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; import { NavigationMenu, @@ -40,6 +41,10 @@ export function Toolbar({ portfolioId }: ToolbarProps) { router.push(`/portfolio/${portfolioId}/summary`); } + function handleClickMeasures() { + router.push(`/portfolio/${portfolioId}/measures`); + } + const [modalIsOpen, setModalIsOpen] = useState(false); const [isRemoteAssessmentOpen, setIsRemoteAssessmentOpen] = useState(false); @@ -62,6 +67,14 @@ export function Toolbar({ portfolioId }: ToolbarProps) { Summary + + + Measures + +
{ } diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx index 2c670bd..014a40a 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx @@ -33,7 +33,7 @@ import { import { PortfolioStatus as PortfolioStatusOptions } from "@/app/db/schema/portfolio"; import { PortfolioGoal as PortfolioGoalOptions } from "@/app/db/schema/portfolio"; import { useSession } from "next-auth/react"; -import PortfolioPlanTable from "@/app/components/portfolio/plan/PlanTable"; +import PortfolioPlanTable from "@/app/components/portfolio/measures/PlanTable"; // dropdown selection component for both goal and status @@ -110,7 +110,7 @@ const updateSettings = async ({ const permissionsData = await permissionsReponse.json(); const permitted = permissionsData.permitted; - console.log("USER IS PERMITTED TO DO THIS!!!!") + console.log("USER IS PERMITTED TO DO THIS!!!!"); // If the user is not permitted to delete the portfolio, we'll throw an error if (!permitted) { throw new Error("User is not permitted to update this portfolio"); @@ -120,7 +120,7 @@ const updateSettings = async ({ // We will create a js object with the starting values // We will then update the values that are not null - const body: bodyType = {} + const body: bodyType = {}; if (name) { body.name = name; @@ -146,8 +146,6 @@ const updateSettings = async ({ "Content-Type": "application/json", }, body: requestBody, - - }); if (!response.ok) { @@ -370,109 +368,153 @@ export default function PortfolioSettings({ // 4) Create the API return ( - -
-
- - - - - Rename the Portfolio:

Permanently change the name of your portfolio

-
- - - - - - -
- - - Change the Portfolio Budget:

The total budget across ALL properties. Works aim to stay within this budget

-
- - handleNumericKeyDown(e)}/> - - - - -
- - - Change the Portfolio Goal:

Adjust the overall aim of the works conducted on this portfolio

-
- - - - - - -
- - - Change the Status of the Portfolio:

Adjust where the portfolio stands in the works pipeline

-
- - - - - - -
-
-
+
+
+ + + + + Rename the Portfolio: +

+ Permanently change the name of your portfolio +

+
+ + + + + + +
+ + + Change the Portfolio Budget: +

+ The total budget across ALL properties. Works aim to stay + within this budget +

+
+ + handleNumericKeyDown(e)} + /> + + + + +
+ + + Change the Portfolio Goal: +

+ Adjust the overall aim of the works conducted on this + portfolio +

+
+ + + + + + +
+ + + Change the Status of the Portfolio: +

+ Adjust where the portfolio stands in the works pipeline +

+
+ + + + + + +
+
+
+
+
+ + Danger Zone: + + + + Delete the Portfolio: +

+ Permanently delete the portfolio and all property data + assigned to this portfolio +

+
+ + + +
+
+
+ + + Are you sure? +

+ To confirm, please type the name of the portfolio ( + {portfolioSettingsData.name}) +

+ setDeleteConfirmationByName(e.target.value)} + placeholder="Type portfolio name" + /> + + + + +
+
+
-
- - Danger Zone: - - - - Delete the Portfolio:

Permanently delete the portfolio and all property data assigned to this portfolio

-
- - - -
-
-
- - - Are you sure? -

- To confirm, please type the name of the portfolio ( - {portfolioSettingsData.name}) -

- setDeleteConfirmationByName(e.target.value)} - placeholder="Type portfolio name" - /> - - - - -
-
-
-
- ); - - } - - - \ No newline at end of file diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index 3cc8dd5..c40c70a 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -449,62 +449,73 @@ export async function getProperties( interface UnaggregatedPortfolioPlanRecommendation { quantity: number; - quantityUnit: string; + quantityUnit: string | null; estimatedCost: number; - materialType: string; + materialType: string | null; propertyId?: string; numberOfProperties?: number; // Optional field to hold the count of unique propertyIds + measureType: string | null; + measureTypeCount: number | undefined; // Optional field to hold the count of unique measureTypes } function aggregateRecommendations( data: UnaggregatedPortfolioPlanRecommendation[] ): PortfolioPlanRecommendation[] { const grouped: { - [key: string]: PortfolioPlanRecommendation & { propertyIds: Set }; + [key: string]: PortfolioPlanRecommendation & { + propertyIds: Set; + measureTypes: Set; + }; } = {}; data.forEach((item) => { - // Use the combination of quantityUnit and materialType as a unique key - const key = `${item.quantityUnit}_${item.materialType}`; + // Use the combination of quantityUnit and materialType (or measureType if materialType is missing) as a unique key + const key = item.materialType + ? `${item.quantityUnit}_${item.materialType}` + : `${item.quantityUnit}_${item.measureType}`; if (!grouped[key]) { grouped[key] = { ...item, numberOfProperties: 0, // Initialize to 0 propertyIds: item.propertyId ? new Set([item.propertyId]) : new Set(), + measureTypes: item.measureType + ? new Set([item.measureType]) + : new Set(), }; } else { grouped[key].quantity += item.quantity; grouped[key].estimatedCost += item.estimatedCost; + if (item.propertyId) { grouped[key].propertyIds.add(item.propertyId); } + + if (item.measureType) { + grouped[key].measureTypes.add(item.measureType); + } } }); - // Round the results to 2 decimal places and compute uniquePropertyCount + // Round the results to 2 decimal places, compute uniquePropertyCount, and count unique measureTypes for (const key in grouped) { grouped[key].quantity = parseFloat(grouped[key].quantity.toFixed(2)); grouped[key].estimatedCost = parseFloat( grouped[key].estimatedCost.toFixed(2) ); grouped[key].numberOfProperties = grouped[key].propertyIds.size; + grouped[key].measureTypeCount = grouped[key].measureTypes.size; + + // Cleanup temporary properties delete (grouped[key] as any).propertyIds; // using type assertion to bypass the TS error + delete (grouped[key] as any).measureTypes; // using type assertion to bypass the TS error } return Object.values(grouped); } -export async function getPortfolioPlan(portfolioId: string) { - // To do this we need to do the following: - // 1. For the portfolioId, get all of the default plans. This can be done from the plan table - // 2. For the plans, get the recommendations. This can be done from the planRecommendation table - // 3. For the recommendations get the materials, the quantity and the cost. - // 4. For the materials, get the type of material - - // There was some trouble performing all of the relations in a single query so we split it into - // three requests (unfortunately) - +export async function getPortfolioMeasures(portfolioId: string) { + // Step 1: Get all default plans for the portfolioId const plans = await db.query.plan.findMany({ where: and( eq(property.portfolioId, BigInt(portfolioId)), @@ -514,6 +525,7 @@ export async function getPortfolioPlan(portfolioId: string) { const planIds = plans.map((plan) => plan.id); + // Step 2: Get recommendations for the plans const recommendations = await db.query.planRecommendations.findMany({ where: inArray(planRecommendations.planId, planIds), }); @@ -522,12 +534,13 @@ export async function getPortfolioPlan(portfolioId: string) { (recommendation) => recommendation.id ); + // Step 3: Get data for recommendations, including materials and measureTypes const data = await db.query.recommendation.findMany({ where: and( inArray(recommendation.id, recommendationIds), eq(recommendation.default, true) ), - columns: { propertyId: true }, + columns: { propertyId: true, measureType: true }, with: { recommendationMaterials: { with: { @@ -546,22 +559,41 @@ export async function getPortfolioPlan(portfolioId: string) { }, }); + // Step 4: Handle recommendations with or without materials const unnestedRecommendations: UnnestedRecommendation[] = data.reduce( (acc: UnnestedRecommendation[], recommendation) => { - const materials = recommendation.recommendationMaterials.map( - (material) => ({ - quantity: material.quantity, - quantityUnit: material.quantityUnit, - estimatedCost: material.estimatedCost, - materialType: material.material.type, + if (recommendation.recommendationMaterials.length === 0) { + // Handle case where no materials are associated + acc.push({ + quantity: 0, + quantityUnit: null, + estimatedCost: 0, + materialType: null, propertyId: String(recommendation.propertyId), - }) - ); - return [...acc, ...materials]; + measureType: recommendation.measureType, // Ensure measureType is included + measureTypeCount: undefined, // Initialize to undefined + }); + } else { + // Process recommendations with materials + const materials = recommendation.recommendationMaterials.map( + (material) => ({ + quantity: material.quantity, + quantityUnit: material.quantityUnit, + estimatedCost: material.estimatedCost, + materialType: material.material.type, + propertyId: String(recommendation.propertyId), + measureType: recommendation.measureType, // Include measureType + measureTypeCount: undefined, // Initialize to undefined + }) + ); + acc.push(...materials); + } + return acc; }, [] ); + // Step 5: Aggregate the recommendations const aggregated = aggregateRecommendations(unnestedRecommendations); return aggregated;