{cardComponent ? (
cardComponent.description
@@ -67,7 +75,7 @@ export default function RecommendationCard({
Estimated Cost: |
{cardComponent
- ? "£" + formatNumber(cardComponent.estimatedCost)
+ ? "£" + formatNumber(cardComponent?.estimatedCost || 0)
: ""}
|
diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx
index ae34bb98..cccb5bb3 100644
--- a/src/app/components/building-passport/RecommendationContainer.tsx
+++ b/src/app/components/building-passport/RecommendationContainer.tsx
@@ -1,8 +1,8 @@
"use client";
import {
- ComponentRecommendation,
Recommendation,
+ RecommendationType,
} from "@/app/db/schema/recommendations";
import RecommendationCard from "./RecommendationCard";
import RecommendationCostSummaryCard from "./RecommendationCostSummaryCard";
@@ -15,7 +15,7 @@ import { RecommendationMetricMap } from "@/types/recommendations";
import RecommendationEpcSummaryCard from "./RecommendationEpcSummaryCard";
interface RecommendationContainerProps {
- recommendations: Recommendation;
+ recommendations: Recommendation[];
propertyMeta: PropertyMeta;
}
@@ -23,28 +23,40 @@ export default function RecommendationContainer({
recommendations,
propertyMeta,
}: RecommendationContainerProps) {
- const defaultWallsRecommendations = recommendations.Walls?.find(
- (rec: ComponentRecommendation) => rec.default
- ) || { estimatedCost: 0, sapPoints: 0 };
+ const categorizedRecommendations = recommendations.reduce((acc, curr) => {
+ const typeKey = curr.type as RecommendationType;
- const defaultFloorRecommendations = recommendations.Floor?.find(
- (rec: ComponentRecommendation) => rec.default
- ) || { estimatedCost: 0, sapPoints: 0 };
+ if (!acc[typeKey]) {
+ acc[typeKey] = [];
+ }
+ acc[typeKey].push(curr);
+ return acc;
+ }, {} as Record
);
- const defaultVentiliationRecommendations = recommendations.Ventilation?.find(
- (rec: ComponentRecommendation) => rec.default
- ) || { estimatedCost: 0, sapPoints: 0 };
+ const defaultWallsRecommendations =
+ categorizedRecommendations.wall_insulation.find(
+ (rec: Recommendation) => rec.default
+ ) || { estimatedCost: 0, sapPoints: 0 };
+
+ const defaultFloorRecommendations =
+ categorizedRecommendations.floor_insulation?.find(
+ (rec: Recommendation) => rec.default
+ ) || { estimatedCost: 0, sapPoints: 0 };
+
+ // const defaultVentiliationRecommendations = recommendations.Ventilation?.find(
+ // (rec: ComponentRecommendation) => rec.default
+ // ) || { estimatedCost: 0, sapPoints: 0 };
const [costMap, setCostMap] = useState({
- Walls: defaultWallsRecommendations.estimatedCost,
- Floor: defaultFloorRecommendations.estimatedCost,
- Ventilation: defaultVentiliationRecommendations.estimatedCost,
+ Walls: defaultWallsRecommendations?.estimatedCost || 0,
+ Floor: defaultFloorRecommendations?.estimatedCost || 0,
+ // Ventilation: defaultVentiliationRecommendations?.estimatedCost || 0,
});
const [sapMap, setSapMap] = useState({
- Walls: defaultWallsRecommendations.sapPoints,
- Floor: defaultFloorRecommendations.sapPoints,
- Ventilation: defaultVentiliationRecommendations.sapPoints,
+ Walls: defaultWallsRecommendations?.sapPoints || 0,
+ Floor: defaultFloorRecommendations.sapPoints || 0,
+ // Ventilation: defaultVentiliationRecommendations.sapPoints,
});
const [totalEstimatedCost, setTotalEstimatedCost] = useState(
@@ -58,7 +70,8 @@ export default function RecommendationContainer({
const currentEpcRating = propertyMeta.currentEpcRating;
const currentSapPoints = propertyMeta.currentSapPoints;
- const expectedSapPoints = currentSapPoints + totalSapPoints;
+ //TODO: Use Math.min while we have dummy SAP points
+ const expectedSapPoints = Math.min(currentSapPoints + totalSapPoints, 100);
const [expectedEpcRating, setExpectedEpcRating] = useState(
sapToEpc(expectedSapPoints)
);
@@ -79,12 +92,13 @@ export default function RecommendationContainer({
- {Object.entries(recommendations).map(
+ {Object.entries(categorizedRecommendations).map(
([componentType, recommendationData], idx) => {
return (
void;
- recommendationData: ComponentRecommendation[];
- setCardComponent: Dispatch>;
+ recommendationData: Recommendation[];
+ setCardComponent: Dispatch>;
setCostMap: Dispatch>;
costMap: RecommendationMetricMap;
setTotalEstimatedCost: Dispatch>;
@@ -88,8 +88,11 @@ export default function RecommendationModal({
const newSapImporvement = sumRecommendationMetricMap(newSapMap);
setTotalSapPoints(newSapImporvement);
+ // TODO: While we have placeholder SAP points, constrain to 100
+ const newSapPoints = Math.min(currentSapPoints + newSapImporvement, 100);
+
// update the expected EPC rating
- setExpectedEpcRating(sapToEpc(currentSapPoints + newSapImporvement));
+ setExpectedEpcRating(sapToEpc(newSapPoints));
}
return (
diff --git a/src/app/components/building-passport/RecommendationTable.tsx b/src/app/components/building-passport/RecommendationTable.tsx
index 1bff68a0..97286e8b 100644
--- a/src/app/components/building-passport/RecommendationTable.tsx
+++ b/src/app/components/building-passport/RecommendationTable.tsx
@@ -16,10 +16,10 @@ import {
TableRow,
} from "@/app/shadcn_components/ui/table";
-import { ComponentRecommendation } from "@/app/db/schema/recommendations";
+import { Recommendation } from "@/app/db/schema/recommendations";
import { Dispatch, SetStateAction, useEffect } from "react";
-interface DataTableProps {
+interface DataTableProps {
columns: ColumnDef[];
data: T[];
defaultRowIndex: number;
@@ -34,7 +34,7 @@ interface DataTableProps {
setSaveButtonDisabled: Dispatch>;
}
-export default function RecommendationTable({
+export default function RecommendationTable({
data,
columns,
defaultRowIndex,
diff --git a/src/app/components/building-passport/RecommendationTableColumns.tsx b/src/app/components/building-passport/RecommendationTableColumns.tsx
index 2eab2684..4fb4b367 100644
--- a/src/app/components/building-passport/RecommendationTableColumns.tsx
+++ b/src/app/components/building-passport/RecommendationTableColumns.tsx
@@ -1,9 +1,9 @@
import { ColumnDef } from "@tanstack/react-table";
import { Checkbox } from "@/app/shadcn_components/ui/checkbox";
import { formatNumber } from "@/app/utils";
-import { ComponentRecommendation } from "@/app/db/schema/recommendations";
+import { Recommendation } from "@/app/db/schema/recommendations";
-const uvalueColumns: ColumnDef[] = [
+const uvalueColumns: ColumnDef[] = [
{
accessorKey: "description",
header: "Description",
diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts
index 4f51e8be..992c93c6 100644
--- a/src/app/db/schema/recommendations.ts
+++ b/src/app/db/schema/recommendations.ts
@@ -12,16 +12,6 @@ import {
import { material } from "./materials";
import { InferModel, relations } from "drizzle-orm";
-export interface ComponentRecommendation {
- id: number;
- type: string;
- description: string;
- estimatedCost: number;
- default: boolean;
- newUValue?: number;
- sapPoints: number;
-}
-
export const recommendation = pgTable("recommendation", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
propertyId: bigint("property_id", { mode: "bigint" })
@@ -120,3 +110,7 @@ export type PlanRecommendations = InferModel<
typeof planRecommendations,
"select"
>;
+
+// We allow recommendation types to be a string however we'll set up a typing for it here to control
+// the types we expect
+export type RecommendationType = "wall_insulation" | "floor_insulation";
diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx
new file mode 100644
index 00000000..bbbee524
--- /dev/null
+++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx
@@ -0,0 +1,24 @@
+import { PropertyMeta } from "@/app/db/schema/property";
+import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer";
+import { getPropertyMeta, getRecommendations } from "../../utils";
+
+export default async function Recommendations({
+ params,
+}: {
+ params: { slug: string; propertyId: string; planId: string };
+}) {
+ const propertyMeta = await getPropertyMeta(params.propertyId);
+
+ const recommendations = await getRecommendations(params.planId);
+
+ console.log(recommendations);
+ return (
+
+ );
+}
diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx
new file mode 100644
index 00000000..dd5bd122
--- /dev/null
+++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx
@@ -0,0 +1,99 @@
+import { getPlans, getPropertyMeta } from "../utils";
+import { formatDateTime, formatNumber, sapToEpc } from "@/app/utils";
+import EpcCard from "@/app/components/building-passport/EpcCard";
+import { Card, CardContent, CardHeader } from "@/app/shadcn_components/ui/card";
+import GoToPlanButton from "@/app/components/building-passport/GoToPlanButton";
+
+function PlanCard({
+ expectedEpcRating,
+ createdAt,
+ totalEstimatedCost,
+ totalSapPoints,
+ planId,
+}: {
+ expectedEpcRating: string;
+ createdAt: Date;
+ totalEstimatedCost: number;
+ totalSapPoints: number;
+ planId: string;
+}) {
+ return (
+
+
+
+
+
+
+
+
+ Total cost:
+ £{formatNumber(totalEstimatedCost)}
+
+
+ Total SAP points:
+ {totalSapPoints}
+
+
+
+
+
+ Created: {formatDateTime(createdAt)}
+
+
+
+
+ );
+}
+
+export default async function RecommendationPlans({
+ params,
+}: {
+ params: { slug: string; propertyId: string };
+}) {
+ const propertyMeta = await getPropertyMeta(params.propertyId);
+ const plans = await getPlans(params.propertyId);
+
+ // TODO: We don't currently have any visual identification of plans that have been set as default vs not
+
+ return (
+
+
Retrofit Plans
+
+
+ {plans.map((plan, index) => {
+ const totalEstimatedCost = plan.planRecommendations.reduce(
+ (acc, rec) => acc + rec.recommendation.estimatedCost,
+ 0
+ );
+ const totalSapPoints = plan.planRecommendations.reduce(
+ (acc, rec) => acc + rec.recommendation.sapPoints,
+ 0
+ );
+
+ // Placeholder while we return 999 for all sap points
+ const expectedSapPoints = Math.min(
+ propertyMeta.currentSapPoints + totalSapPoints,
+ 100
+ );
+
+ const expectedEpcRating = sapToEpc(expectedSapPoints);
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/utils.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/utils.tsx
new file mode 100644
index 00000000..d3f77e42
--- /dev/null
+++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/utils.tsx
@@ -0,0 +1,10 @@
+import { RecommendationMetricMap } from "@/types/recommendations";
+
+export function sumRecommendationMetricMap(
+ obj: RecommendationMetricMap
+): number {
+ // In the recommendations section of the building passport we have the cost map which
+ // contains the costs of the recommendations. We need to sum these costs to display
+ // the total cost of the recommendations
+ return Object.values(obj).reduce((sum, current) => sum + current, 0);
+}
diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts
index 67064c8c..9cfba534 100644
--- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts
+++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts
@@ -1,5 +1,8 @@
import { recommendation } from "./../../../../db/schema/recommendations";
-import { columns } from "./../../components/propertyTableColumns";
+import {
+ Recommendation,
+ planRecommendations,
+} from "@/app/db/schema/recommendations";
import { db } from "@/app/db/db";
import {
Feature,
@@ -12,6 +15,31 @@ import { plan, Plan } from "@/app/db/schema/recommendations";
import { getRating } from "@/app/utils";
import { eq } from "drizzle-orm";
+type RecommendationList = {
+ recommendation: Recommendation;
+}[];
+
+export async function getRecommendations(
+ planId: string
+): Promise {
+ const data = (await db.query.planRecommendations.findMany({
+ where: eq(planRecommendations.planId, BigInt(planId)),
+ columns: {},
+ with: {
+ recommendation: true,
+ },
+ })) as RecommendationList;
+
+ if (!data) {
+ throw new Error("Network response was not ok");
+ }
+
+ // unnest the recommendations
+ const recommendations = data.map((item) => item.recommendation);
+
+ return recommendations;
+}
+
type PlanRelation = Plan & {
planRecommendations: {
recommendation: { estimatedCost: number; sapPoints: number };
diff --git a/src/types/recommendations.ts b/src/types/recommendations.ts
index 09e7047d..3934a0b6 100644
--- a/src/types/recommendations.ts
+++ b/src/types/recommendations.ts
@@ -1,5 +1,6 @@
export interface RecommendationMetricMap {
Walls: number;
Floor: number;
- Ventilation: number;
+ // TODO: Implement ventilation
+ // Ventilation: number;
}
diff --git a/tailwind.config.js b/tailwind.config.js
index 9731b113..f70ada24 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -27,6 +27,7 @@ module.exports = {
hoverblue: "#3e4073",
brandtan: "#d3b488",
hovertan: "#947750",
+ brandgold: "#f1bb06",
brandbrown: "#3d1e05",
brandmidblue: "#3943b7",
border: "hsl(var(--border))",