From 9fd9d0641e814a2a04676b3482e57b19ff314978 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 16 Aug 2023 11:58:51 +0100 Subject: [PATCH 01/13] added property_id to plan table --- .../db/migrations/0035_perfect_tenebrous.sql | 6 + src/app/db/migrations/meta/0035_snapshot.json | 1272 +++++++++++++++++ src/app/db/migrations/meta/_journal.json | 7 + src/app/db/schema/recommendations.ts | 3 + tsconfig.json | 4 +- 5 files changed, 1290 insertions(+), 2 deletions(-) create mode 100644 src/app/db/migrations/0035_perfect_tenebrous.sql create mode 100644 src/app/db/migrations/meta/0035_snapshot.json diff --git a/src/app/db/migrations/0035_perfect_tenebrous.sql b/src/app/db/migrations/0035_perfect_tenebrous.sql new file mode 100644 index 0000000..fb9dab1 --- /dev/null +++ b/src/app/db/migrations/0035_perfect_tenebrous.sql @@ -0,0 +1,6 @@ +ALTER TABLE "plan" ADD COLUMN "property_id" bigint NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "plan" ADD CONSTRAINT "plan_property_id_property_id_fk" FOREIGN KEY ("property_id") REFERENCES "property"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/app/db/migrations/meta/0035_snapshot.json b/src/app/db/migrations/meta/0035_snapshot.json new file mode 100644 index 0000000..1cc6b1a --- /dev/null +++ b/src/app/db/migrations/meta/0035_snapshot.json @@ -0,0 +1,1272 @@ +{ + "version": "5", + "dialect": "pg", + "id": "976fba8c-569b-4257-a6fe-c0ae0d9e0e01", + "prevId": "2508c58f-bee6-44b6-9617-464c78540cf6", + "tables": { + "material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depths": { + "name": "depths", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": { + "cost_unit": { + "name": "cost_unit", + "values": { + "gbp_sq_meter": "gbp_sq_meter" + } + }, + "depth_unit": { + "name": "depth_unit", + "values": { + "mm": "mm" + } + }, + "type": { + "name": "type", + "values": { + "suspended_floor_insulation": "suspended_floor_insulation", + "solid_floor_insulation": "solid_floor_insulation", + "external_wall_insulation": "external_wall_insulation", + "internal_wall_insulation": "internal_wall_insulation" + } + }, + "r_value_unit": { + "name": "r_value_unit", + "values": { + "square_meter_kelvin_per_watt": "square_meter_kelvin_per_watt" + } + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "values": { + "watt_per_meter_kelvin": "watt_per_meter_kelvin" + } + }, + "goal": { + "name": "goal", + "values": { + "Valuation Improvement": "Valuation Improvement", + "Increasing EPC": "Increasing EPC", + "Reducing CO2 emissions": "Reducing CO2 emissions", + "Energy Savings": "Energy Savings", + "None": "None" + } + }, + "role": { + "name": "role", + "values": { + "creator": "creator", + "admin": "admin", + "read": "read", + "write": "write" + } + }, + "status": { + "name": "status", + "values": { + "scoping": "scoping", + "assessment": "assessment", + "tendering": "tendering", + "project underway": "project underway", + "completion; status: on track": "completion; status: on track", + "completion; status: delayed": "completion; status: delayed", + "completion; status: at risk": "completion; status: at risk", + "completion; status: completed": "completion; status: completed", + "needs review": "needs review" + } + }, + "epc": { + "name": "epc", + "values": { + "A": "A", + "B": "B", + "C": "C", + "D": "D", + "E": "E", + "F": "F", + "G": "G" + } + }, + "creation_status": { + "name": "creation_status", + "values": { + "LOADING": "LOADING", + "READY": "READY", + "ERROR": "ERROR" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index 6d8a230..393f773 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1692012985856, "tag": "0034_wandering_nick_fury", "breakpoints": true + }, + { + "idx": 35, + "version": "5", + "when": 1692183485471, + "tag": "0035_perfect_tenebrous", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index 7eb9ea2..3197839 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -70,6 +70,9 @@ export const plan = pgTable("plan", { portfolioId: bigint("portfolio_id", { mode: "bigint" }) .notNull() .references(() => portfolio.id), + propertyId: bigint("property_id", { mode: "bigint" }) + .notNull() + .references(() => property.id), createdAt: timestamp("created_at").notNull().defaultNow(), isDefault: boolean("is_default").notNull(), }); diff --git a/tsconfig.json b/tsconfig.json index 6468155..7f10c94 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, From 5e70c072a5ef751341c4331178da71c768e454b7 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 16 Aug 2023 14:33:14 +0100 Subject: [PATCH 02/13] Creating plans UI (WIP) --- src/app/db/schema/recommendations.ts | 44 ++++- .../[propertyId]/recommendations/page.tsx | 169 +++++++++++------- .../building-passport/[propertyId]/utils.ts | 38 ++++ 3 files changed, 176 insertions(+), 75 deletions(-) diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index 3197839..4f51e8b 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -10,6 +10,7 @@ import { bigint, } from "drizzle-orm/pg-core"; import { material } from "./materials"; +import { InferModel, relations } from "drizzle-orm"; export interface ComponentRecommendation { id: number; @@ -21,12 +22,6 @@ export interface ComponentRecommendation { sapPoints: number; } -export interface Recommendation { - Walls?: ComponentRecommendation[]; - Ventilation?: ComponentRecommendation[]; - Floor?: ComponentRecommendation[]; -} - export const recommendation = pgTable("recommendation", { id: bigserial("id", { mode: "bigint" }).primaryKey(), propertyId: bigint("property_id", { mode: "bigint" }) @@ -88,3 +83,40 @@ export const planRecommendations = pgTable("plan_recommendations", { .notNull() .references(() => recommendation.id), }); + +// create a one to many relation to map a plan to the details in the underlying recommendation +// create a many to many map from a plan to a recommendation +// A recommendation can be in multiple plans and therefore we have a many to many relationship between +// plan and recommendations. This relationship is facilitated by the planRecommdnations table + +export const planRelations = relations(plan, ({ many }) => ({ + planRecommendations: many(planRecommendations), +})); + +export const planRecommendationsRelations = relations( + planRecommendations, + ({ one }) => ({ + plan: one(plan, { + fields: [planRecommendations.planId], + references: [plan.id], + }), + recommendation: one(recommendation, { + fields: [planRecommendations.recommendationId], + references: [recommendation.id], + }), + }) +); + +export const recommendationRelations = relations( + planRecommendations, + ({ many }) => ({ + planRecommendations: many(planRecommendations), + }) +); + +export type Plan = InferModel; +export type Recommendation = InferModel; +export type PlanRecommendations = InferModel< + typeof planRecommendations, + "select" +>; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx index a47c737..b3f81c8 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx @@ -1,7 +1,7 @@ import { Recommendation } from "@/app/db/schema/recommendations"; import { PropertyMeta } from "@/app/db/schema/property"; import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer"; -import { getPropertyMeta } from "../utils"; +import { getPlans, getPropertyMeta } from "../utils"; export default async function Recommendations({ params, @@ -9,77 +9,108 @@ export default async function Recommendations({ params: { slug: string; propertyId: string }; }) { const propertyMeta = await getPropertyMeta(params.propertyId); - - const recommendations: Recommendation = { - Walls: [ - { - id: 1, - type: "internal_wall_insulation", - description: "140mm Mineral Wool internal wall insulation", - estimatedCost: 9_450, - default: true, - newUValue: 0.29, - sapPoints: 4, - }, - { - id: 2, - type: "internal_wall_insulation", - description: "30mm Vacuum Insulation Panels wall insulation", - estimatedCost: 10_135, - default: false, - newUValue: 0.28, - sapPoints: 12, - }, - { - id: 3, - type: "internal_external_wall_insulation", - description: - "80mm Mineral Wool External Wall Insulation and 30mm rigid insulation internal wall insulation", - estimatedCost: 13_450, - default: false, - newUValue: 0.25, - sapPoints: 14, - }, - ], - Ventilation: [ - { - id: 4, - type: "mechanical_ventilation", - description: "Two decentralised mechanical ventilation units", - estimatedCost: 750, - default: true, - sapPoints: -2, - }, - ], - Floor: [ - { - id: 5, - type: "suspended_floor_insulation", - description: "70mm Rigid insulation foam boards with floor screed", - estimatedCost: 3_450, - default: true, - newUValue: 0.24, - sapPoints: 7, - }, - { - id: 5, - type: "suspended_floor_insulation", - description: "90mm Rigid insulation foam boards with floor screed", - estimatedCost: 4_120, - default: true, - newUValue: 0.24, - sapPoints: 7, - }, - ], - }; + const plans = await getPlans(params.propertyId); + console.log("PLANS", plans); + console.log(plans[0].planRecommendations); return (
-
Recommendations
- +
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 + ); + + return ( +
+
Created At: {plan.createdAt.toISOString()}
+
Is Default: {plan.isDefault ? "Yes" : "No"}
+
Total Estimated Cost: £{totalEstimatedCost}
+
Total SAP Points: {totalSapPoints}
+
+ ); + })} +
); + + // const recommendations: Recommendation = { + // Walls: [ + // { + // id: 1, + // type: "internal_wall_insulation", + // description: "140mm Mineral Wool internal wall insulation", + // estimatedCost: 9_450, + // default: true, + // newUValue: 0.29, + // sapPoints: 4, + // }, + // { + // id: 2, + // type: "internal_wall_insulation", + // description: "30mm Vacuum Insulation Panels wall insulation", + // estimatedCost: 10_135, + // default: false, + // newUValue: 0.28, + // sapPoints: 12, + // }, + // { + // id: 3, + // type: "internal_external_wall_insulation", + // description: + // "80mm Mineral Wool External Wall Insulation and 30mm rigid insulation internal wall insulation", + // estimatedCost: 13_450, + // default: false, + // newUValue: 0.25, + // sapPoints: 14, + // }, + // ], + // Ventilation: [ + // { + // id: 4, + // type: "mechanical_ventilation", + // description: "Two decentralised mechanical ventilation units", + // estimatedCost: 750, + // default: true, + // sapPoints: -2, + // }, + // ], + // Floor: [ + // { + // id: 5, + // type: "suspended_floor_insulation", + // description: "70mm Rigid insulation foam boards with floor screed", + // estimatedCost: 3_450, + // default: true, + // newUValue: 0.24, + // sapPoints: 7, + // }, + // { + // id: 5, + // type: "suspended_floor_insulation", + // description: "90mm Rigid insulation foam boards with floor screed", + // estimatedCost: 4_120, + // default: true, + // newUValue: 0.24, + // sapPoints: 7, + // }, + // ], + // }; + + // return ( + //
+ //
Recommendations
+ // + //
+ // ); } diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index 2cb729c..75e0110 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -1,3 +1,5 @@ +import { recommendation } from "./../../../../db/schema/recommendations"; +import { columns } from "./../../components/propertyTableColumns"; import { db } from "@/app/db/db"; import { Feature, @@ -6,9 +8,45 @@ import { PropertyMeta, propertyDetailsEpc, } from "@/app/db/schema/property"; +import { plan, Plan } from "@/app/db/schema/recommendations"; import { getRating } from "@/app/utils"; import { eq } from "drizzle-orm"; +// type PlanRelation = Plan & { +// planRecommendations: PlanRecommendations; +// }; + +type PlanRelation = Plan & { + planRecommendations: { + recommendation: { estimatedCost: number; sapPoints: number }; + }[]; +}; + +export async function getPlans(propertyId: string): Promise { + const data = await db.query.plan.findMany({ + where: eq(plan.propertyId, BigInt(propertyId)), + with: { + planRecommendations: { + columns: {}, + with: { + recommendation: { + columns: { + estimatedCost: true, + sapPoints: true, + }, + }, + }, + }, + }, + }); + + if (!data) { + throw new Error("Network response was not ok"); + } + + return data as PlanRelation[]; +} + export async function getPropertyMeta( propertyId: string ): Promise { From 316e8382359a69ac56a810b02c21f378ae359f29 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 16 Aug 2023 14:56:15 +0100 Subject: [PATCH 03/13] Added total sap points --- .../[propertyId]/recommendations/page.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx index b3f81c8..5a53dd5 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx @@ -2,6 +2,7 @@ import { Recommendation } from "@/app/db/schema/recommendations"; import { PropertyMeta } from "@/app/db/schema/property"; import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer"; import { getPlans, getPropertyMeta } from "../utils"; +import { sapToEpc } from "@/app/utils"; export default async function Recommendations({ params, @@ -28,12 +29,21 @@ export default async function Recommendations({ 0 ); + // Placeholder while we return 999 for all sap points + const expectedSapPoints = Math.min( + propertyMeta.currentSapPoints + totalSapPoints, + 100 + ); + + const expectedEpcRating = sapToEpc(expectedSapPoints); + return (
Created At: {plan.createdAt.toISOString()}
Is Default: {plan.isDefault ? "Yes" : "No"}
Total Estimated Cost: £{totalEstimatedCost}
Total SAP Points: {totalSapPoints}
+
Expected EP Rating: {expectedEpcRating}
); })} From c64969bd078967fec9f59b514c8e94b27237e7c3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 16 Aug 2023 16:28:44 +0100 Subject: [PATCH 04/13] implemented layout of plan card --- .../components/building-passport/EpcCard.tsx | 21 +++++- .../[propertyId]/recommendations/page.tsx | 74 ++++++++++++++++--- src/app/utils.ts | 2 +- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/app/components/building-passport/EpcCard.tsx b/src/app/components/building-passport/EpcCard.tsx index ef0f0aa..086b85f 100644 --- a/src/app/components/building-passport/EpcCard.tsx +++ b/src/app/components/building-passport/EpcCard.tsx @@ -1,18 +1,35 @@ export default function EpcCard({ epcRating, fullMargin = true, + expected = false, }: { epcRating: string; fullMargin: boolean; + expected?: boolean; }) { let marginClass = ""; if (fullMargin) { marginClass = "mx-auto"; } + let title; + let bgStyling; + if (expected) { + title = "Expected Energy Rating"; + bgStyling = "bg-green-600"; + } else { + title = "Energy Rating"; + bgStyling = "bg-brandblue"; + } + return ( -
-
Energy Rating
+
+
{title}
{epcRating}
); diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx index 5a53dd5..4249c99 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx @@ -2,7 +2,63 @@ import { Recommendation } from "@/app/db/schema/recommendations"; import { PropertyMeta } from "@/app/db/schema/property"; import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer"; import { getPlans, getPropertyMeta } from "../utils"; -import { sapToEpc } from "@/app/utils"; +import { formatDateTime, formatNumber, sapToEpc } from "@/app/utils"; +import EpcCard from "@/app/components/building-passport/EpcCard"; +import { ChevronRight } from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/app/shadcn_components/ui/card"; +import { Button } from "@/app/shadcn_components/ui/button"; + +function PlanCard({ + expectedEpcRating, + createdAt, + totalEstimatedCost, + totalSapPoints, +}: { + expectedEpcRating: string; + createdAt: Date; + totalEstimatedCost: number; + totalSapPoints: number; +}) { + return ( + +
+ +
+
+ + + + + + Created: {formatDateTime(createdAt)} + +
+ Total cost: + £{formatNumber(totalEstimatedCost)} +
+
+ Total SAP points: + {totalSapPoints} +
+
+
+
+ ); +} export default async function Recommendations({ params, @@ -18,7 +74,7 @@ export default async function Recommendations({
Retrofit Plans
-
+
{plans.map((plan, index) => { const totalEstimatedCost = plan.planRecommendations.reduce( (acc, rec) => acc + rec.recommendation.estimatedCost, @@ -38,13 +94,13 @@ export default async function Recommendations({ const expectedEpcRating = sapToEpc(expectedSapPoints); return ( -
-
Created At: {plan.createdAt.toISOString()}
-
Is Default: {plan.isDefault ? "Yes" : "No"}
-
Total Estimated Cost: £{totalEstimatedCost}
-
Total SAP Points: {totalSapPoints}
-
Expected EP Rating: {expectedEpcRating}
-
+ ); })}
diff --git a/src/app/utils.ts b/src/app/utils.ts index 35c5c64..8a099ef 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -47,7 +47,7 @@ export function sapToEpc(sapPoints: number): string { } } -export function formatDateTime(dateTimeString: string): string { +export function formatDateTime(dateTimeString: string | Date): string { // Create a new Date object const dateTime = new Date(dateTimeString); From 3b9602b293cc98ac7b4deb81d9913464af038730 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 16 Aug 2023 17:23:52 +0100 Subject: [PATCH 05/13] Setting up plans page --- .../components/building-passport/EpcCard.tsx | 2 +- .../RecommendationContainer.tsx | 2 +- .../building-passport/RecommendationModal.tsx | 2 +- .../components/building-passport/Toolbar.tsx | 8 +- .../pre-assessment-report/page.tsx | 53 +++-- .../[propertyId]/recommendations/page.tsx | 182 ------------------ .../[propertyId]/recommendations/utils.tsx | 10 - .../building-passport/[propertyId]/utils.ts | 4 - 8 files changed, 30 insertions(+), 233 deletions(-) delete mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx delete mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx diff --git a/src/app/components/building-passport/EpcCard.tsx b/src/app/components/building-passport/EpcCard.tsx index 086b85f..4925a0b 100644 --- a/src/app/components/building-passport/EpcCard.tsx +++ b/src/app/components/building-passport/EpcCard.tsx @@ -25,7 +25,7 @@ export default function EpcCard({ return (
diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx index d4ee30f..ae34bb9 100644 --- a/src/app/components/building-passport/RecommendationContainer.tsx +++ b/src/app/components/building-passport/RecommendationContainer.tsx @@ -10,7 +10,7 @@ import { Separator } from "@/app/shadcn_components/ui/separator"; import { PropertyMeta } from "@/app/db/schema/property"; import { sapToEpc } from "@/app/utils"; import { useState } from "react"; -import { sumRecommendationMetricMap } from "@/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils"; +import { sumRecommendationMetricMap } from "@/app/portfolio/[slug]/building-passport/[propertyId]/plans/utils"; import { RecommendationMetricMap } from "@/types/recommendations"; import RecommendationEpcSummaryCard from "./RecommendationEpcSummaryCard"; diff --git a/src/app/components/building-passport/RecommendationModal.tsx b/src/app/components/building-passport/RecommendationModal.tsx index 5081c15..08e3971 100644 --- a/src/app/components/building-passport/RecommendationModal.tsx +++ b/src/app/components/building-passport/RecommendationModal.tsx @@ -4,7 +4,7 @@ import { Dispatch, Fragment, SetStateAction, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; import RecommendationTable from "@/app/components/building-passport/RecommendationTable"; import { RecommendationMetricMap } from "@/types/recommendations"; -import { sumRecommendationMetricMap } from "@/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils"; +import { sumRecommendationMetricMap } from "@/app/portfolio/[slug]/building-passport/[propertyId]/plans/utils"; import uvalueColumns from "./RecommendationTableColumns"; import { sapToEpc } from "@/app/utils"; diff --git a/src/app/components/building-passport/Toolbar.tsx b/src/app/components/building-passport/Toolbar.tsx index ced6cf9..655c86d 100644 --- a/src/app/components/building-passport/Toolbar.tsx +++ b/src/app/components/building-passport/Toolbar.tsx @@ -46,10 +46,10 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) { const recommendationsButton = ( - Retrofit Recommendations + Retrofit Plans ); @@ -66,13 +66,13 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) { {preAssessmentReportButton} {recommendationsButton} - Plan optimiser - + */} +
{address}
); @@ -35,6 +35,9 @@ interface PropertyDetailsCardProps { }; } +const rowTitleStyle = "text-gray-100 align-top pb-3"; +const rowValueStyle = "text-gray-100 text-end pr-8 pt-1 align-top pb-3"; + function PropertyDetailsCard({ conditionReportData, propertyMeta, @@ -45,33 +48,29 @@ function PropertyDetailsCard({ .join(" "); return ( -
-
+
+
- +
- - + + - - + + - - + - - + @@ -81,26 +80,20 @@ function PropertyDetailsCard({
Year built: - {propertyMeta.yearBuilt} - Year built:{propertyMeta.yearBuilt}
Property Type: - {propertyText} - Property Type:{propertyText}
Total floor area: + Total floor area: {`${conditionReportData.totalFloorArea} m`} 2
In conservation area: + In conservation area: {propertyDetailsSpatial.inConservationArea}
- - + + - - + + - - + + - - + diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx deleted file mode 100644 index 4249c99..0000000 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/page.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { Recommendation } from "@/app/db/schema/recommendations"; -import { PropertyMeta } from "@/app/db/schema/property"; -import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer"; -import { getPlans, getPropertyMeta } from "../utils"; -import { formatDateTime, formatNumber, sapToEpc } from "@/app/utils"; -import EpcCard from "@/app/components/building-passport/EpcCard"; -import { ChevronRight } from "lucide-react"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/app/shadcn_components/ui/card"; -import { Button } from "@/app/shadcn_components/ui/button"; - -function PlanCard({ - expectedEpcRating, - createdAt, - totalEstimatedCost, - totalSapPoints, -}: { - expectedEpcRating: string; - createdAt: Date; - totalEstimatedCost: number; - totalSapPoints: number; -}) { - return ( - -
- -
-
- - - - - - Created: {formatDateTime(createdAt)} - -
- Total cost: - £{formatNumber(totalEstimatedCost)} -
-
- Total SAP points: - {totalSapPoints} -
-
-
-
- ); -} - -export default async function Recommendations({ - params, -}: { - params: { slug: string; propertyId: string }; -}) { - const propertyMeta = await getPropertyMeta(params.propertyId); - const plans = await getPlans(params.propertyId); - console.log("PLANS", plans); - console.log(plans[0].planRecommendations); - - 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 ( - - ); - })} -
-
- ); - - // const recommendations: Recommendation = { - // Walls: [ - // { - // id: 1, - // type: "internal_wall_insulation", - // description: "140mm Mineral Wool internal wall insulation", - // estimatedCost: 9_450, - // default: true, - // newUValue: 0.29, - // sapPoints: 4, - // }, - // { - // id: 2, - // type: "internal_wall_insulation", - // description: "30mm Vacuum Insulation Panels wall insulation", - // estimatedCost: 10_135, - // default: false, - // newUValue: 0.28, - // sapPoints: 12, - // }, - // { - // id: 3, - // type: "internal_external_wall_insulation", - // description: - // "80mm Mineral Wool External Wall Insulation and 30mm rigid insulation internal wall insulation", - // estimatedCost: 13_450, - // default: false, - // newUValue: 0.25, - // sapPoints: 14, - // }, - // ], - // Ventilation: [ - // { - // id: 4, - // type: "mechanical_ventilation", - // description: "Two decentralised mechanical ventilation units", - // estimatedCost: 750, - // default: true, - // sapPoints: -2, - // }, - // ], - // Floor: [ - // { - // id: 5, - // type: "suspended_floor_insulation", - // description: "70mm Rigid insulation foam boards with floor screed", - // estimatedCost: 3_450, - // default: true, - // newUValue: 0.24, - // sapPoints: 7, - // }, - // { - // id: 5, - // type: "suspended_floor_insulation", - // description: "90mm Rigid insulation foam boards with floor screed", - // estimatedCost: 4_120, - // default: true, - // newUValue: 0.24, - // sapPoints: 7, - // }, - // ], - // }; - - // return ( - //
- //
Recommendations
- // - //
- // ); -} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx deleted file mode 100644 index d3f77e4..0000000 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 75e0110..67064c8 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -12,10 +12,6 @@ import { plan, Plan } from "@/app/db/schema/recommendations"; import { getRating } from "@/app/utils"; import { eq } from "drizzle-orm"; -// type PlanRelation = Plan & { -// planRecommendations: PlanRecommendations; -// }; - type PlanRelation = Plan & { planRecommendations: { recommendation: { estimatedCost: number; sapPoints: number }; From e0d252e89a5cef1afa15d965e3c46ad884c5d94f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 16 Aug 2023 18:55:42 +0100 Subject: [PATCH 06/13] Implemented recommendations into front end --- .../building-passport/GoToPlanButton.tsx | 24 +++++ .../building-passport/RecommendationCard.tsx | 24 +++-- .../RecommendationContainer.tsx | 54 ++++++---- .../building-passport/RecommendationModal.tsx | 11 ++- .../building-passport/RecommendationTable.tsx | 6 +- .../RecommendationTableColumns.tsx | 4 +- src/app/db/schema/recommendations.ts | 14 +-- .../[propertyId]/plans/[planId]/page.tsx | 24 +++++ .../[propertyId]/plans/page.tsx | 99 +++++++++++++++++++ .../[propertyId]/plans/utils.tsx | 10 ++ .../building-passport/[propertyId]/utils.ts | 30 +++++- src/types/recommendations.ts | 3 +- tailwind.config.js | 1 + 13 files changed, 255 insertions(+), 49 deletions(-) create mode 100644 src/app/components/building-passport/GoToPlanButton.tsx create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/plans/utils.tsx diff --git a/src/app/components/building-passport/GoToPlanButton.tsx b/src/app/components/building-passport/GoToPlanButton.tsx new file mode 100644 index 0000000..ef9d921 --- /dev/null +++ b/src/app/components/building-passport/GoToPlanButton.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Button } from "@/app/shadcn_components/ui/button"; +import { ChevronRight } from "lucide-react"; +import { usePathname, useRouter } from "next/navigation"; + +export default function GoToPlanButton({ planId }: { planId: string }) { + const router = useRouter(); + const pathname = usePathname(); + + function handleOnClick() { + router.push(`${pathname}/${planId}`); + } + + return ( + + ); +} diff --git a/src/app/components/building-passport/RecommendationCard.tsx b/src/app/components/building-passport/RecommendationCard.tsx index 0a9ccc6..650b7f3 100644 --- a/src/app/components/building-passport/RecommendationCard.tsx +++ b/src/app/components/building-passport/RecommendationCard.tsx @@ -1,5 +1,8 @@ "use client"; -import { ComponentRecommendation } from "@/app/db/schema/recommendations"; +import type { + Recommendation, + RecommendationType, +} from "@/app/db/schema/recommendations"; import { Dispatch, SetStateAction, useState } from "react"; import { formatNumber } from "@/app/utils"; import { RecommendationMetricMap } from "@/types/recommendations"; @@ -10,6 +13,11 @@ const selectionStyling = const noSelectionStyling = "shadow active:shadow active:bg-brandmidblue w-full border rounded p-4 cursor-pointer text-gray-300 bg-white hover:bg-hoverblue hover:text-gray-100 transition-colors rounded-md flex flex-col justify-start"; +const TitleMap = { + wall_insulation: "Wall Insulation", + floor_insulation: "Floor Insulation", +}; + export default function RecommendationCard({ componentType, recommendationData, @@ -22,8 +30,8 @@ export default function RecommendationCard({ currentSapPoints, setExpectedEpcRating, }: { - componentType: string; - recommendationData: ComponentRecommendation[]; + componentType: RecommendationType; + recommendationData: Recommendation[]; setCostMap: Dispatch>; costMap: RecommendationMetricMap; setTotalEstimatedCost: Dispatch>; @@ -34,11 +42,11 @@ export default function RecommendationCard({ setExpectedEpcRating: Dispatch>; }) { const defaultComponent = recommendationData.find( - (rec: ComponentRecommendation) => rec.default - ) as ComponentRecommendation; + (rec: Recommendation) => rec.default + ) as Recommendation; const [cardComponent, setCardComponent] = - useState(defaultComponent); + useState(defaultComponent); const [modalIsOpen, setModalIsOpen] = useState(false); @@ -49,7 +57,7 @@ export default function RecommendationCard({ setModalIsOpen(true); }} > -

{componentType}

+

{TitleMap[componentType]}

{cardComponent ? ( cardComponent.description @@ -67,7 +75,7 @@ export default function RecommendationCard({
diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx index ae34bb9..cccb5bb 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 1bff68a..97286e8 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 2eab268..4fb4b36 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 4f51e8b..992c93c 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 0000000..bbbee52 --- /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 ( +
+
Recommendations
+ +
+ ); +} 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 0000000..dd5bd12 --- /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 0000000..d3f77e4 --- /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 67064c8..9cfba53 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 09e7047..3934a0b 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 9731b11..f70ada2 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))", From 788aac7b83f6b774b717b7c213f8cf66f3901bd1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 13:29:48 +0100 Subject: [PATCH 07/13] Added layout for portfolio only --- src/app/components/portfolio/Toolbar.tsx | 23 ++++++++++++-- .../portfolio/[slug]/(portfolio)/layout.tsx | 31 +++++++++++++++++++ .../[slug]/{ => (portfolio)}/page.tsx | 18 +---------- .../[slug]/(portfolio)/plan/page.tsx | 7 +++++ .../[propertyId]/plans/[planId]/page.tsx | 2 -- .../building-passport/[propertyId]/utils.ts | 2 +- 6 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/layout.tsx rename src/app/portfolio/[slug]/{ => (portfolio)}/page.tsx (91%) create mode 100644 src/app/portfolio/[slug]/(portfolio)/plan/page.tsx diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index 4d85b71..f525e08 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -1,6 +1,10 @@ "use client"; -import { Cog6ToothIcon, CalculatorIcon } from "@heroicons/react/24/outline"; +import { + Cog6ToothIcon, + CalculatorIcon, + BuildingOfficeIcon, +} from "@heroicons/react/24/outline"; import { NavigationMenu, NavigationMenuItem, @@ -10,6 +14,7 @@ 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"; interface ToolbarProps { portfolioId: string; @@ -20,12 +25,18 @@ const navigationMenuTriggerStyle = cva( ); export function Toolbar({ portfolioId }: ToolbarProps) { + const router = useRouter(); + function handleClickSettings() { console.log("Settings were clicked, implement me"); } function handleClickPortfolioPlan() { - console.log("Opt Plan was clicked, implement me"); + router.push(`/portfolio/${portfolioId}/plan`); + } + + function handleClickPortfolio() { + router.push(`/portfolio/${portfolioId}`); } const [modalIsOpen, setModalIsOpen] = useState(false); @@ -33,6 +44,14 @@ export function Toolbar({ portfolioId }: ToolbarProps) { return ( + + + Portfolio + + +
+

+ {portfolioName} +

+
+
+
+
+ +
+
+
+ {children} + + ); +} diff --git a/src/app/portfolio/[slug]/page.tsx b/src/app/portfolio/[slug]/(portfolio)/page.tsx similarity index 91% rename from src/app/portfolio/[slug]/page.tsx rename to src/app/portfolio/[slug]/(portfolio)/page.tsx index f1bbd8c..0dcc3d8 100644 --- a/src/app/portfolio/[slug]/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/page.tsx @@ -1,5 +1,5 @@ import { HomeIcon } from "@heroicons/react/24/outline"; -import { getPortfolio, getProperties } from "./utils"; +import { getPortfolio, getProperties } from "../utils"; import { Toolbar } from "@/app/components/portfolio/Toolbar"; import DataTable from "@/app/portfolio/[slug]/components/propertyTable"; import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns"; @@ -187,12 +187,8 @@ export default async function Page({ }) { // This page is served from the server so we can make calls to the database - // This is temp until we retrieve this data from the frontend - // TODO: Update the objects to contains objective + any other required information - const portfolioId = params.slug; const { - name: portfolioName, budget, cost: totalCost, co2EquivalentSavings, @@ -212,18 +208,6 @@ export default async function Page({ return ( <> -
-

- {portfolioName} -

-
-
-
-
- -
-
-
diff --git a/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx b/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx new file mode 100644 index 0000000..c01caf2 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx @@ -0,0 +1,7 @@ +export default async function PortfolioPlan({ + params, +}: { + params: { slug: string; propertyId: string; planId: string }; +}) { + return
Portfolio Plan
; +} 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 index bbbee52..06d0a90 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx @@ -8,10 +8,8 @@ export default async function Recommendations({ params: { slug: string; propertyId: string; planId: string }; }) { const propertyMeta = await getPropertyMeta(params.propertyId); - const recommendations = await getRecommendations(params.planId); - console.log(recommendations); return (
Recommendations
diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index 9cfba53..f7c8201 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -1,4 +1,4 @@ -import { recommendation } from "./../../../../db/schema/recommendations"; +import { recommendation } from "../../../../db/schema/recommendations"; import { Recommendation, planRecommendations, From 495fbd05f677c647b37d35df3c20f86b937d19ad Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 14:08:46 +0100 Subject: [PATCH 08/13] Adding quantity, quantity_unit and estimated_cost to recommendationsMaterial table --- .../building-passport/RecommendationCard.tsx | 2 +- src/app/db/migrations/0036_real_stryfe.sql | 9 + src/app/db/migrations/meta/0036_snapshot.json | 1296 +++++++++++++++++ src/app/db/migrations/meta/_journal.json | 7 + src/app/db/schema/recommendations.ts | 7 + src/app/portfolio/[slug]/utils.ts | 12 + 6 files changed, 1332 insertions(+), 1 deletion(-) create mode 100644 src/app/db/migrations/0036_real_stryfe.sql create mode 100644 src/app/db/migrations/meta/0036_snapshot.json diff --git a/src/app/components/building-passport/RecommendationCard.tsx b/src/app/components/building-passport/RecommendationCard.tsx index 650b7f3..a2b5148 100644 --- a/src/app/components/building-passport/RecommendationCard.tsx +++ b/src/app/components/building-passport/RecommendationCard.tsx @@ -1,5 +1,5 @@ "use client"; -import type { +import { Recommendation, RecommendationType, } from "@/app/db/schema/recommendations"; diff --git a/src/app/db/migrations/0036_real_stryfe.sql b/src/app/db/migrations/0036_real_stryfe.sql new file mode 100644 index 0000000..306555a --- /dev/null +++ b/src/app/db/migrations/0036_real_stryfe.sql @@ -0,0 +1,9 @@ +DO $$ BEGIN + CREATE TYPE "unit_quantity" AS ENUM('meters_squared'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +ALTER TABLE "recommendation_materials" ADD COLUMN "quantity" real NOT NULL;--> statement-breakpoint +ALTER TABLE "recommendation_materials" ADD COLUMN "quantity_unit" "unit_quantity" NOT NULL;--> statement-breakpoint +ALTER TABLE "recommendation_materials" ADD COLUMN "estimated_cost" real NOT NULL; \ No newline at end of file diff --git a/src/app/db/migrations/meta/0036_snapshot.json b/src/app/db/migrations/meta/0036_snapshot.json new file mode 100644 index 0000000..820b9ad --- /dev/null +++ b/src/app/db/migrations/meta/0036_snapshot.json @@ -0,0 +1,1296 @@ +{ + "version": "5", + "dialect": "pg", + "id": "2063f8e7-e1b2-486a-8e64-6a8a24b631d2", + "prevId": "976fba8c-569b-4257-a6fe-c0ae0d9e0e01", + "tables": { + "material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depths": { + "name": "depths", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "quantity_unit": { + "name": "quantity_unit", + "type": "unit_quantity", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": { + "cost_unit": { + "name": "cost_unit", + "values": { + "gbp_sq_meter": "gbp_sq_meter" + } + }, + "depth_unit": { + "name": "depth_unit", + "values": { + "mm": "mm" + } + }, + "type": { + "name": "type", + "values": { + "suspended_floor_insulation": "suspended_floor_insulation", + "solid_floor_insulation": "solid_floor_insulation", + "external_wall_insulation": "external_wall_insulation", + "internal_wall_insulation": "internal_wall_insulation" + } + }, + "r_value_unit": { + "name": "r_value_unit", + "values": { + "square_meter_kelvin_per_watt": "square_meter_kelvin_per_watt" + } + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "values": { + "watt_per_meter_kelvin": "watt_per_meter_kelvin" + } + }, + "goal": { + "name": "goal", + "values": { + "Valuation Improvement": "Valuation Improvement", + "Increasing EPC": "Increasing EPC", + "Reducing CO2 emissions": "Reducing CO2 emissions", + "Energy Savings": "Energy Savings", + "None": "None" + } + }, + "role": { + "name": "role", + "values": { + "creator": "creator", + "admin": "admin", + "read": "read", + "write": "write" + } + }, + "status": { + "name": "status", + "values": { + "scoping": "scoping", + "assessment": "assessment", + "tendering": "tendering", + "project underway": "project underway", + "completion; status: on track": "completion; status: on track", + "completion; status: delayed": "completion; status: delayed", + "completion; status: at risk": "completion; status: at risk", + "completion; status: completed": "completion; status: completed", + "needs review": "needs review" + } + }, + "epc": { + "name": "epc", + "values": { + "A": "A", + "B": "B", + "C": "C", + "D": "D", + "E": "E", + "F": "F", + "G": "G" + } + }, + "creation_status": { + "name": "creation_status", + "values": { + "LOADING": "LOADING", + "READY": "READY", + "ERROR": "ERROR" + } + }, + "unit_quantity": { + "name": "unit_quantity", + "values": { + "meters_squared": "meters_squared" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index 393f773..cf8a899 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -253,6 +253,13 @@ "when": 1692183485471, "tag": "0035_perfect_tenebrous", "breakpoints": true + }, + { + "idx": 36, + "version": "5", + "when": 1692623295104, + "tag": "0036_real_stryfe", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index 992c93c..6b9d0b7 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -8,6 +8,7 @@ import { real, boolean, bigint, + pgEnum, } from "drizzle-orm/pg-core"; import { material } from "./materials"; import { InferModel, relations } from "drizzle-orm"; @@ -36,6 +37,9 @@ export const recommendation = pgTable("recommendation", { totalWorkHours: real("total_work_hours"), }); +export const unitQuantity: [string, ...string[]] = ["meters_squared"]; +export const unitQuantityEnum = pgEnum("unit_quantity", unitQuantity); + export const recommendationMaterials = pgTable("recommendation_materials", { id: bigserial("id", { mode: "bigint" }).primaryKey(), recommendationId: bigint("recommendation_id", { @@ -48,6 +52,9 @@ export const recommendationMaterials = pgTable("recommendation_materials", { .references(() => material.id), createdAt: timestamp("created_at").notNull().defaultNow(), depth: real("depth"), + quantity: real("quantity").notNull(), + quantityUnit: unitQuantityEnum("quantity_unit").notNull(), + estimatedCost: real("estimated_cost").notNull(), }); export const plan = pgTable("plan", { diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index 2c76306..6191553 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -50,3 +50,15 @@ export async function getProperties( return data; } + +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 + // I need to make the following updates: + // 1) Add quanitity to the recommendationMaterials table + // 2) Add estimatedCost to the recommendationMaterials table. We want the cost here to be the cost associated to + // the material type +} From 0bcf1e0a3c055366a7f4f2e2e3e72cf514222e1c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 14:15:05 +0100 Subject: [PATCH 09/13] changed quantity_unit value --- .../migrations/0037_awesome_harry_osborn.sql | 1 + src/app/db/migrations/meta/0037_snapshot.json | 1296 +++++++++++++++++ src/app/db/migrations/meta/_journal.json | 7 + src/app/db/schema/recommendations.ts | 2 +- 4 files changed, 1305 insertions(+), 1 deletion(-) create mode 100644 src/app/db/migrations/0037_awesome_harry_osborn.sql create mode 100644 src/app/db/migrations/meta/0037_snapshot.json diff --git a/src/app/db/migrations/0037_awesome_harry_osborn.sql b/src/app/db/migrations/0037_awesome_harry_osborn.sql new file mode 100644 index 0000000..01a1588 --- /dev/null +++ b/src/app/db/migrations/0037_awesome_harry_osborn.sql @@ -0,0 +1 @@ +ALTER TYPE "unit_quantity" ADD VALUE 'm2'; \ No newline at end of file diff --git a/src/app/db/migrations/meta/0037_snapshot.json b/src/app/db/migrations/meta/0037_snapshot.json new file mode 100644 index 0000000..b2d5069 --- /dev/null +++ b/src/app/db/migrations/meta/0037_snapshot.json @@ -0,0 +1,1296 @@ +{ + "version": "5", + "dialect": "pg", + "id": "ae896627-43c0-4d11-972f-a042af3bf845", + "prevId": "2063f8e7-e1b2-486a-8e64-6a8a24b631d2", + "tables": { + "material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depths": { + "name": "depths", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "quantity_unit": { + "name": "quantity_unit", + "type": "unit_quantity", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": { + "cost_unit": { + "name": "cost_unit", + "values": { + "gbp_sq_meter": "gbp_sq_meter" + } + }, + "depth_unit": { + "name": "depth_unit", + "values": { + "mm": "mm" + } + }, + "type": { + "name": "type", + "values": { + "suspended_floor_insulation": "suspended_floor_insulation", + "solid_floor_insulation": "solid_floor_insulation", + "external_wall_insulation": "external_wall_insulation", + "internal_wall_insulation": "internal_wall_insulation" + } + }, + "r_value_unit": { + "name": "r_value_unit", + "values": { + "square_meter_kelvin_per_watt": "square_meter_kelvin_per_watt" + } + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "values": { + "watt_per_meter_kelvin": "watt_per_meter_kelvin" + } + }, + "goal": { + "name": "goal", + "values": { + "Valuation Improvement": "Valuation Improvement", + "Increasing EPC": "Increasing EPC", + "Reducing CO2 emissions": "Reducing CO2 emissions", + "Energy Savings": "Energy Savings", + "None": "None" + } + }, + "role": { + "name": "role", + "values": { + "creator": "creator", + "admin": "admin", + "read": "read", + "write": "write" + } + }, + "status": { + "name": "status", + "values": { + "scoping": "scoping", + "assessment": "assessment", + "tendering": "tendering", + "project underway": "project underway", + "completion; status: on track": "completion; status: on track", + "completion; status: delayed": "completion; status: delayed", + "completion; status: at risk": "completion; status: at risk", + "completion; status: completed": "completion; status: completed", + "needs review": "needs review" + } + }, + "epc": { + "name": "epc", + "values": { + "A": "A", + "B": "B", + "C": "C", + "D": "D", + "E": "E", + "F": "F", + "G": "G" + } + }, + "creation_status": { + "name": "creation_status", + "values": { + "LOADING": "LOADING", + "READY": "READY", + "ERROR": "ERROR" + } + }, + "unit_quantity": { + "name": "unit_quantity", + "values": { + "m2": "m2" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index cf8a899..4ee5be6 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1692623295104, "tag": "0036_real_stryfe", "breakpoints": true + }, + { + "idx": 37, + "version": "5", + "when": 1692623678223, + "tag": "0037_awesome_harry_osborn", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index 6b9d0b7..a771acf 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -37,7 +37,7 @@ export const recommendation = pgTable("recommendation", { totalWorkHours: real("total_work_hours"), }); -export const unitQuantity: [string, ...string[]] = ["meters_squared"]; +export const unitQuantity: [string, ...string[]] = ["m2"]; export const unitQuantityEnum = pgEnum("unit_quantity", unitQuantity); export const recommendationMaterials = pgTable("recommendation_materials", { From 011d98fe48cdb6e6eab7141604808587ef78506f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 18:44:31 +0100 Subject: [PATCH 10/13] formatting the return from the portfolio plan --- src/app/db/schema/recommendations.ts | 28 ++++++- .../[slug]/(portfolio)/plan/page.tsx | 9 ++- src/app/portfolio/[slug]/utils.ts | 76 +++++++++++++++++-- tsconfig.json | 4 +- 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index a771acf..78a8d92 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -104,10 +104,34 @@ export const planRecommendationsRelations = relations( }) ); +// We construct a relationship between a recommendation and recommendationMaterials +// On recommendationMaterial will map to a single recommendation + +// Define the relationships for the recommendation table export const recommendationRelations = relations( - planRecommendations, + recommendation, ({ many }) => ({ - planRecommendations: many(planRecommendations), + recommendationMaterials: many(recommendationMaterials), + }) +); + +// Define the relationships for the material table +export const materialRelations = relations(material, ({ many }) => ({ + recommendationMaterials: many(recommendationMaterials), +})); + +// Define the relationships for the recommendationMaterials table +export const recommendationMaterialsRelations = relations( + recommendationMaterials, + ({ one }) => ({ + recommendation: one(recommendation, { + fields: [recommendationMaterials.recommendationId], + references: [recommendation.id], + }), + material: one(material, { + fields: [recommendationMaterials.materialId], + references: [material.id], + }), }) ); diff --git a/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx b/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx index c01caf2..866d18b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx @@ -1,7 +1,12 @@ +import { getPortfolioPlan } from "../../utils"; + export default async function PortfolioPlan({ params, }: { - params: { slug: string; propertyId: string; planId: string }; + params: { slug: string }; }) { - return
Portfolio Plan
; + const portfolioId = params.slug; + const portfolioPlan = await getPortfolioPlan(portfolioId); + + return
{String(portfolioPlan)}
; } diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index 6191553..f5019c8 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -1,9 +1,19 @@ -import { eq } from "drizzle-orm"; +import { recommendation } from "./../../db/schema/recommendations"; +import { material } from "./../../db/schema/materials"; +import { and, eq, inArray } from "drizzle-orm"; import { db } from "@/app/db/db"; import { portfolio } from "@/app/db/schema/portfolio"; import { property } from "@/app/db/schema/property"; import type { Portfolio } from "@/app/db/schema/portfolio"; import type { PropertyWithTarget } from "@/app/db/schema/property"; +import { plan, planRecommendations } from "@/app/db/schema/recommendations"; + +type UnnestedRecommendation = { + quantity: number; + quantityUnit: string; + estimatedCost: number; + materialType: string; +}; export async function getPortfolio(portfolioId: string): Promise { const data = await db @@ -57,8 +67,64 @@ export async function getPortfolioPlan(portfolioId: string) { // 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 - // I need to make the following updates: - // 1) Add quanitity to the recommendationMaterials table - // 2) Add estimatedCost to the recommendationMaterials table. We want the cost here to be the cost associated to - // the material type + + // There was some trouble performing all of the relations in a single query so we split it into + // three requests (unfortunately) + + const plans = await db.query.plan.findMany({ + where: and( + eq(property.portfolioId, BigInt(portfolioId)), + eq(plan.isDefault, true) + ), + }); + + const planIds = plans.map((plan) => plan.id); + + const recommendations = await db.query.planRecommendations.findMany({ + where: inArray(planRecommendations.planId, planIds), + }); + + const recommendationIds = recommendations.map( + (recommendation) => recommendation.id + ); + + const data = await db.query.recommendation.findMany({ + where: and( + inArray(recommendation.id, recommendationIds), + eq(recommendation.default, true) + ), + with: { + recommendationMaterials: { + with: { + material: { + columns: { + type: true, + }, + }, + }, + columns: { + quantity: true, + quantityUnit: true, + estimatedCost: true, + }, + }, + }, + }); + + 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, + }) + ); + return [...acc, ...materials]; + }, + [] + ); + + return unnestedRecommendations; } diff --git a/tsconfig.json b/tsconfig.json index 7f10c94..6468155 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, From 35a1629336144b17c1167afffec57a35f629543a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 18:53:15 +0100 Subject: [PATCH 11/13] aggregate results --- src/app/portfolio/[slug]/utils.ts | 40 ++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index f5019c8..adbf53a 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -61,6 +61,41 @@ export async function getProperties( return data; } +interface Recommendation { + quantity: number; + quantityUnit: string; + estimatedCost: number; + materialType: string; +} + +function aggregateRecommendations(data: Recommendation[]): Recommendation[] { + const grouped: { [key: string]: Recommendation } = {}; + + data.forEach((item) => { + // Use the combination of quantityUnit and materialType as a unique key + const key = `${item.quantityUnit}_${item.materialType}`; + + if (!grouped[key]) { + grouped[key] = { + ...item, + }; + } else { + grouped[key].quantity += item.quantity; + grouped[key].estimatedCost += item.estimatedCost; + } + }); + + // Round the results to 2 decimal places + for (const key in grouped) { + grouped[key].quantity = parseFloat(grouped[key].quantity.toFixed(2)); + grouped[key].estimatedCost = parseFloat( + grouped[key].estimatedCost.toFixed(2) + ); + } + + 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 @@ -126,5 +161,8 @@ export async function getPortfolioPlan(portfolioId: string) { [] ); - return unnestedRecommendations; + const aggregated = aggregateRecommendations(unnestedRecommendations); + console.log(aggregated); + + return aggregated; } From 3c37a6e15e42e5f0b2073b9293ae3db4b28253de Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 19:28:07 +0100 Subject: [PATCH 12/13] added number of properties aggregation --- .../components/portfolio/plan/PlanTable.tsx | 84 +++++++++++++++++++ .../portfolio/plan/PlanTableColumns.tsx | 24 ++++++ src/app/db/schema/recommendations.ts | 16 ++++ src/app/portfolio/[slug]/utils.ts | 38 +++++---- 4 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 src/app/components/portfolio/plan/PlanTable.tsx create mode 100644 src/app/components/portfolio/plan/PlanTableColumns.tsx diff --git a/src/app/components/portfolio/plan/PlanTable.tsx b/src/app/components/portfolio/plan/PlanTable.tsx new file mode 100644 index 0000000..d711dbc --- /dev/null +++ b/src/app/components/portfolio/plan/PlanTable.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { + flexRender, + getCoreRowModel, + useReactTable, + ColumnDef, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/app/shadcn_components/ui/table"; + +import { Feature, GeneralFeature } from "@/app/db/schema/property"; + +interface DataTableProps { + columns: ColumnDef[]; + data: T[]; +} + +export default function FeatureTable({ + data, + columns, +}: DataTableProps) { + // Initialise the table + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+
Local Authority: - {propertyMeta.localAuthority} - Local Authority:{propertyMeta.localAuthority}
Constituency: - {propertyMeta.constituency} - Constituency:{propertyMeta.constituency}
Tenure - {propertyMeta.tenure} - Tenure{propertyMeta.tenure}
Number of rooms: + Number of rooms: {propertyMeta.numberOfRooms || "unkown"}
Estimated Cost: {cardComponent - ? "£" + formatNumber(cardComponent.estimatedCost) + ? "£" + formatNumber(cardComponent?.estimatedCost || 0) : ""}
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/src/app/components/portfolio/plan/PlanTableColumns.tsx b/src/app/components/portfolio/plan/PlanTableColumns.tsx new file mode 100644 index 0000000..da705e2 --- /dev/null +++ b/src/app/components/portfolio/plan/PlanTableColumns.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { PortfolioPlanRecommendation } from "@/app/db/schema/recommendations"; +import { Badge } from "@/app/shadcn_components/ui/badge"; +import { ColumnDef } from "@tanstack/react-table"; + +export const retrofitColumns: ColumnDef[] = [ + { + accessorKey: "materialType", + header: "Retrofit Measure", + }, + { + accessorKey: "Number o", + header: "Number of Properties", + }, + { + accessorKey: "quantity", + header: "Number of Properties", + }, + { + accessorKey: "estimatedCost", + header: "Estimated Cost", + }, +]; diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index 78a8d92..40df900 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -145,3 +145,19 @@ export type PlanRecommendations = InferModel< // 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"; + +export type UnnestedRecommendation = { + quantity: number; + quantityUnit: string; + estimatedCost: number; + materialType: string; + propertyId: string; +}; + +export interface PortfolioPlanRecommendation { + quantity: number; + quantityUnit: string; + estimatedCost: number; + materialType: string; + numberOfProperties: number; +} diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index adbf53a..ac170be 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -1,5 +1,8 @@ -import { recommendation } from "./../../db/schema/recommendations"; -import { material } from "./../../db/schema/materials"; +import { + recommendation, + UnnestedRecommendation, + PortfolioPlanRecommendation, +} from "./../../db/schema/recommendations"; import { and, eq, inArray } from "drizzle-orm"; import { db } from "@/app/db/db"; import { portfolio } from "@/app/db/schema/portfolio"; @@ -8,13 +11,6 @@ import type { Portfolio } from "@/app/db/schema/portfolio"; import type { PropertyWithTarget } from "@/app/db/schema/property"; import { plan, planRecommendations } from "@/app/db/schema/recommendations"; -type UnnestedRecommendation = { - quantity: number; - quantityUnit: string; - estimatedCost: number; - materialType: string; -}; - export async function getPortfolio(portfolioId: string): Promise { const data = await db .select() @@ -61,15 +57,21 @@ export async function getProperties( return data; } -interface Recommendation { +interface UnaggregatedPortfolioPlanRecommendation { quantity: number; quantityUnit: string; estimatedCost: number; materialType: string; + propertyId?: string; + numberOfProperties?: number; // Optional field to hold the count of unique propertyIds } -function aggregateRecommendations(data: Recommendation[]): Recommendation[] { - const grouped: { [key: string]: Recommendation } = {}; +function aggregateRecommendations( + data: UnaggregatedPortfolioPlanRecommendation[] +): PortfolioPlanRecommendation[] { + const grouped: { + [key: string]: PortfolioPlanRecommendation & { propertyIds: Set }; + } = {}; data.forEach((item) => { // Use the combination of quantityUnit and materialType as a unique key @@ -78,19 +80,26 @@ function aggregateRecommendations(data: Recommendation[]): Recommendation[] { if (!grouped[key]) { grouped[key] = { ...item, + numberOfProperties: 0, // Initialize to 0 + propertyIds: item.propertyId ? new Set([item.propertyId]) : new Set(), }; } else { grouped[key].quantity += item.quantity; grouped[key].estimatedCost += item.estimatedCost; + if (item.propertyId) { + grouped[key].propertyIds.add(item.propertyId); + } } }); - // Round the results to 2 decimal places + // Round the results to 2 decimal places and compute uniquePropertyCount 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; + delete (grouped[key] as any).propertyIds; // using type assertion to bypass the TS error } return Object.values(grouped); @@ -128,6 +137,7 @@ export async function getPortfolioPlan(portfolioId: string) { inArray(recommendation.id, recommendationIds), eq(recommendation.default, true) ), + columns: { propertyId: true }, with: { recommendationMaterials: { with: { @@ -154,6 +164,7 @@ export async function getPortfolioPlan(portfolioId: string) { quantityUnit: material.quantityUnit, estimatedCost: material.estimatedCost, materialType: material.material.type, + propertyId: String(recommendation.propertyId), }) ); return [...acc, ...materials]; @@ -162,7 +173,6 @@ export async function getPortfolioPlan(portfolioId: string) { ); const aggregated = aggregateRecommendations(unnestedRecommendations); - console.log(aggregated); return aggregated; } From 2195fe45284136165d0072529c318e7b4cac6984 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 21 Aug 2023 19:44:22 +0100 Subject: [PATCH 13/13] Adding basic portfolio plan table --- .../components/portfolio/plan/PlanTable.tsx | 11 ++--- .../portfolio/plan/PlanTableColumns.tsx | 48 +++++++++++++++++-- .../[slug]/(portfolio)/plan/page.tsx | 15 +++++- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/app/components/portfolio/plan/PlanTable.tsx b/src/app/components/portfolio/plan/PlanTable.tsx index d711dbc..fc3c956 100644 --- a/src/app/components/portfolio/plan/PlanTable.tsx +++ b/src/app/components/portfolio/plan/PlanTable.tsx @@ -16,17 +16,16 @@ import { TableRow, } from "@/app/shadcn_components/ui/table"; -import { Feature, GeneralFeature } from "@/app/db/schema/property"; +import { PortfolioPlanRecommendation } from "@/app/db/schema/recommendations"; -interface DataTableProps { +interface DataTableProps { columns: ColumnDef[]; data: T[]; } -export default function FeatureTable({ - data, - columns, -}: DataTableProps) { +export default function PortfolioPlanTable< + T extends PortfolioPlanRecommendation +>({ data, columns }: DataTableProps) { // Initialise the table const table = useReactTable({ diff --git a/src/app/components/portfolio/plan/PlanTableColumns.tsx b/src/app/components/portfolio/plan/PlanTableColumns.tsx index da705e2..dee5a59 100644 --- a/src/app/components/portfolio/plan/PlanTableColumns.tsx +++ b/src/app/components/portfolio/plan/PlanTableColumns.tsx @@ -1,24 +1,62 @@ "use client"; import { PortfolioPlanRecommendation } from "@/app/db/schema/recommendations"; -import { Badge } from "@/app/shadcn_components/ui/badge"; +import { formatNumber } from "@/app/utils"; import { ColumnDef } from "@tanstack/react-table"; -export const retrofitColumns: ColumnDef[] = [ +const formattedQuantity = { + m2: "m²", +}; + +const formattedMeasure = { + internal_wall_insulation: "Internal Wall Insulation", + external_wall_insulation: "External Wall Insulation", +}; + +export const portfolioPlanColumns: ColumnDef[] = [ { accessorKey: "materialType", header: "Retrofit Measure", + cell: ({ row }) => { + return ( +
+ { + formattedMeasure[ + row.original.materialType as keyof typeof formattedMeasure + ] + } +
+ ); + }, }, { - accessorKey: "Number o", - header: "Number of Properties", + accessorKey: "numberOfProperties", + header: "Number of properties", + cell: ({ row }) => { + return ( +
{row.original.numberOfProperties}
+ ); + }, }, { accessorKey: "quantity", - header: "Number of Properties", + header: "Quantity required", + cell: ({ row }) => { + return ( +
+ {row.original.quantity + + formattedQuantity[ + row.original.quantityUnit as keyof typeof formattedQuantity + ]} +
+ ); + }, }, { accessorKey: "estimatedCost", header: "Estimated Cost", + cell: ({ row }) => { + return
{"£" + formatNumber(row.original.estimatedCost)}
; + }, }, ]; diff --git a/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx b/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx index 866d18b..74e8b32 100644 --- a/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/plan/page.tsx @@ -1,4 +1,6 @@ +import PortfolioPlanTable from "@/app/components/portfolio/plan/PlanTable"; import { getPortfolioPlan } from "../../utils"; +import { portfolioPlanColumns } from "@/app/components/portfolio/plan/PlanTableColumns"; export default async function PortfolioPlan({ params, @@ -8,5 +10,16 @@ export default async function PortfolioPlan({ const portfolioId = params.slug; const portfolioPlan = await getPortfolioPlan(portfolioId); - return
{String(portfolioPlan)}
; + return ( + <> +
+ { + + } +
+ + ); }