diff --git a/src/app/api/plan/trigger/route.ts b/src/app/api/plan/trigger/route.ts index 9746b39..bb18f7c 100644 --- a/src/app/api/plan/trigger/route.ts +++ b/src/app/api/plan/trigger/route.ts @@ -19,6 +19,7 @@ const PresignedUrlBodySchema = z.object({ already_installed_file_path: z.string().optional(), // optional scenario_id to link the plan to an existing scenario scenario_id: z.string().optional().nullable(), + file_type: z.enum(["csv", "xlsx"]).optional(), // Specify the file type }); export async function POST(request: NextRequest) { @@ -54,8 +55,6 @@ export async function POST(request: NextRequest) { console.log("Triggering plan with body: ", validatedBody); const url = `${process.env.FASTAPI_API_URL}/v1/plan/trigger`; - console.log("Triggering plan with url: ", url); - console.log("Triggering plan with headers: ", headers); const response = await fetch(url, { method: "POST", diff --git a/src/app/api/upload/validate/route.ts b/src/app/api/upload/validate/route.ts index c0c2f5d..8f3526e 100644 --- a/src/app/api/upload/validate/route.ts +++ b/src/app/api/upload/validate/route.ts @@ -10,23 +10,70 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "No file provided" }, { status: 400 }); } - console.log("MADE IT HERE 0"); - console.log(file, file.constructor.name); - const arrayBuffer = await file.arrayBuffer(); - console.log("MADE IT HERE"); - const workbook = XLSX.read(arrayBuffer, { type: "array" }); + const filename = file.name.toLowerCase(); + const mimeType = file.type; - const sheetNames = workbook.SheetNames; + let fileType: "csv" | "xlsx" | "unknown" = "unknown"; - if (sheetNames.length > 1) { + if ( + filename.endsWith(".csv") || + mimeType === "text/csv" || + mimeType === "application/vnd.ms-excel" + ) { + fileType = "csv"; + } else if ( + filename.endsWith(".xlsx") || + filename.endsWith(".xls") || + mimeType === + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) { + fileType = "xlsx"; + } + + if (fileType === "unknown") { return NextResponse.json( - { error: "Multiple sheets not allowed." }, + { error: "Unsupported file type", file_type: fileType }, { status: 400 } ); } - // TODO: We can check if we have a standardised asset list and if so, check which tabs have that option and give the user - // the chance to select which tab they want to use + let isStandardised = false; + let sheetNames: string[] = []; - return NextResponse.json({ message: "Valid file." }); + // Handle CSV + if (fileType === "csv") { + const text = await file.text(); + const lines = text.split("\n").map((line) => line.split(",")); + const headers = lines[0].map((h) => h.trim()); + isStandardised = headers.includes("domna_property_id"); + } + + // Handle Excel + if (fileType === "xlsx") { + const arrayBuffer = await file.arrayBuffer(); + + // Only load specific sheet to reduce memory usage + const workbook = XLSX.read(arrayBuffer, { + type: "array", + sheets: ["Standardised Asset List"], + }); + + sheetNames = workbook.SheetNames; + + const worksheet = workbook.Sheets["Standardised Asset List"]; + if (worksheet) { + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + const headers = jsonData[0] as string[]; + isStandardised = headers?.includes("domna_property_id"); + } + } + + return NextResponse.json({ + message: isStandardised + ? "Standardised Asset List format detected." + : "Valid file. No standardised format detected.", + isStandardised, + file_type: fileType, + ...(fileType === "xlsx" && isStandardised ? { sheetNames } : {}), + }); } diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index 1ba07b6..1f3b811 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -102,6 +102,7 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) { isOpen={modalIsOpen} setIsOpen={setModalIsOpen} portfolioId={portfolioId} + scenarios={scenarios} /> ); diff --git a/src/app/db/migrations/0101_bent_stature.sql b/src/app/db/migrations/0101_bent_stature.sql new file mode 100644 index 0000000..9e639d3 --- /dev/null +++ b/src/app/db/migrations/0101_bent_stature.sql @@ -0,0 +1 @@ +ALTER TABLE "scenario" ADD COLUMN "ashp_cop" real DEFAULT 2.8; \ No newline at end of file diff --git a/src/app/db/migrations/meta/0101_snapshot.json b/src/app/db/migrations/meta/0101_snapshot.json new file mode 100644 index 0000000..ad89845 --- /dev/null +++ b/src/app/db/migrations/meta/0101_snapshot.json @@ -0,0 +1,2995 @@ +{ + "version": "5", + "dialect": "pg", + "id": "09c524ee-2913-4f6e-8efc-95efa1ec9c0e", + "prevId": "55fba457-401b-4f68-9b8a-a674b1975251", + "tables": { + "energy_assessments": { + "name": "energy_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency": { + "name": "current_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_energy_rating": { + "name": "current_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address1": { + "name": "address1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address2": { + "name": "address2", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address3": { + "name": "address3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posttown": { + "name": "posttown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency_label": { + "name": "constituency_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_light_count": { + "name": "low_energy_fixed_light_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_energy_eff": { + "name": "mainheat_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_env_eff": { + "name": "windows_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_energy_eff": { + "name": "lighting_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_potential": { + "name": "environment_impact_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatcont_description": { + "name": "mainheatcont_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_energy_eff": { + "name": "sheating_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority_label": { + "name": "local_authority_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solar_water_heating_flag": { + "name": "solar_water_heating_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_description": { + "name": "floor_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_open_fireplaces": { + "name": "number_open_fireplaces", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_description": { + "name": "windows_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazed_area": { + "name": "glazed_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "mains_gas_flag": { + "name": "mains_gas_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emiss_curr_per_floor_area": { + "name": "co2_emiss_curr_per_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_storey_count": { + "name": "flat_storey_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_energy_eff": { + "name": "roof_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_description": { + "name": "roof_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_energy_eff": { + "name": "floor_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_habitable_rooms": { + "name": "number_habitable_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_env_eff": { + "name": "hot_water_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_energy_eff": { + "name": "mainheatc_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_fuel": { + "name": "main_fuel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_env_eff": { + "name": "lighting_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_energy_eff": { + "name": "windows_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_env_eff": { + "name": "floor_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_env_eff": { + "name": "sheating_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_description": { + "name": "lighting_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_env_eff": { + "name": "roof_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_energy_eff": { + "name": "walls_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_supply": { + "name": "photo_supply", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_env_eff": { + "name": "mainheat_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_glaze_proportion": { + "name": "multi_glaze_proportion", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_controls": { + "name": "main_heating_controls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_top_storey": { + "name": "flat_top_storey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secondheat_description": { + "name": "secondheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_env_eff": { + "name": "walls_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_count": { + "name": "extension_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_env_eff": { + "name": "mainheatc_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lmk_key": { + "name": "lmk_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wind_turbine_count": { + "name": "wind_turbine_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_level": { + "name": "floor_level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_efficiency": { + "name": "potential_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_rating": { + "name": "potential_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_energy_eff": { + "name": "hot_water_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "low_energy_lighting": { + "name": "low_energy_lighting", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_description": { + "name": "walls_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hotwater_description": { + "name": "hotwater_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "lodgement_datetime": { + "name": "lodgement_datetime", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + }, + "mainheat_description": { + "name": "mainheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "glazed_type": { + "name": "glazed_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_location": { + "name": "file_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_company": { + "name": "surveyor_company", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_heating_kwh": { + "name": "space_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "water_heating_kwh": { + "name": "water_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_of_doors": { + "name": "number_of_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_insulated_doors": { + "name": "number_of_insulated_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_floors": { + "name": "number_of_floors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulation_wall_area": { + "name": "insulation_wall_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter": { + "name": "heat_loss_perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length": { + "name": "party_wall_length", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "perimeter": { + "name": "perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rooms_with_bath_and_or_shower": { + "name": "rooms_with_bath_and_or_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rooms_with_mixer_shower_no_bath": { + "name": "rooms_with_mixer_shower_no_bath", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_with_bath_and_mixer_shower": { + "name": "room_with_bath_and_mixer_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draftproofed": { + "name": "percent_draftproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_type": { + "name": "cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_thickness": { + "name": "cylinder_insulation_thickness", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cylinder_thermostat": { + "name": "cylinder_thermostat", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "main_dwelling_ground_floor_area": { + "name": "main_dwelling_ground_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_windows": { + "name": "number_of_windows", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_area": { + "name": "windows_area", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "energy_assessment_documents": { + "name": "energy_assessment_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "document_type", + "primaryKey": false, + "notNull": true + }, + "document_location": { + "name": "document_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk": { + "name": "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessment_scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "energy_assessment_scenarios": { + "name": "energy_assessment_scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scenario_name": { + "name": "scenario_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_scenarios", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "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 + }, + "depth": { + "name": "depth", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "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 + }, + "prime_material_cost": { + "name": "prime_material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "material_cost": { + "name": "material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_cost": { + "name": "labour_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_hours_per_unit": { + "name": "labour_hours_per_unit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plant_cost": { + "name": "plant_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_installer_quote": { + "name": "is_installer_quote", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "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 + }, + "labour_days": { + "name": "labour_days", + "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()" + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "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": {} + }, + "non_intrusive_survey": { + "name": "non_intrusive_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "survey_date": { + "name": "survey_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "non_intrusive_survey_notes": { + "name": "non_intrusive_survey_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk": { + "name": "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk", + "tableFrom": "non_intrusive_survey_notes", + "tableTo": "non_intrusive_survey", + "columnsFrom": [ + "survey_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 + }, + "current_valuation": { + "name": "current_valuation", + "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 + }, + "current_energy_demand": { + "name": "current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand_heating_hotwater": { + "name": "current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "appliances_cost_current": { + "name": "appliances_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "gas_standing_charge": { + "name": "gas_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "electricity_standing_charge": { + "name": "electricity_standing_charge", + "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_details_spatial": { + "name": "property_details_spatial", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "x_coordinate": { + "name": "x_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "y_coordinate": { + "name": "y_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "conservation_status": { + "name": "conservation_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed_building": { + "name": "is_listed_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_heritage_building": { + "name": "is_heritage_building", + "type": "boolean", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "valuation_increase_lower_bound": { + "name": "valuation_increase_lower_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_upper_bound": { + "name": "valuation_increase_upper_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_average": { + "name": "valuation_increase_average", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "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" + }, + "plan_scenario_id_scenario_id_fk": { + "name": "plan_scenario_id_scenario_id_fk", + "tableFrom": "plan", + "tableTo": "scenario", + "columnsFrom": [ + "scenario_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 + }, + "measure_type": { + "name": "measure_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "kwh_savings": { + "name": "kwh_savings", + "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 + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "already_installed": { + "name": "already_installed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": 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": {} + }, + "scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "housing_type": { + "name": "housing_type", + "type": "housing_type", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "primaryKey": false, + "notNull": true + }, + "goal_value": { + "name": "goal_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ashp_cop": { + "name": "ashp_cop", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 2.8 + }, + "trigger_file_path": { + "name": "trigger_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "already_installed_file_path": { + "name": "already_installed_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patches_file_path": { + "name": "patches_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "non_invasive_recommendations_file_path": { + "name": "non_invasive_recommendations_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "multi_plan": { + "name": "multi_plan", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_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 + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_portfolio_id_portfolio_id_fk": { + "name": "scenario_portfolio_id_portfolio_id_fk", + "tableFrom": "scenario", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {} + }, + "solar": { + "name": "solar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "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()" + }, + "google_api_response": { + "name": "google_api_response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "solar_scenario": { + "name": "solar_scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "solar_id": { + "name": "solar_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "scenario_type", + "primaryKey": false, + "notNull": true + }, + "number_panels": { + "name": "number_panels", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "array_kwhp": { + "name": "array_kwhp", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "lifetime_dc_kwh": { + "name": "lifetime_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "yearly_dc_kwh": { + "name": "yearly_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "lifetime_ac_kwh": { + "name": "lifetime_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "yearly_ac_kwh": { + "name": "yearly_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "expected_payback_years": { + "name": "expected_payback_years", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "panelled_roof_area": { + "name": "panelled_roof_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "solar_scenario_solar_id_solar_id_fk": { + "name": "solar_scenario_solar_id_solar_id_fk", + "tableFrom": "solar_scenario", + "tableTo": "solar", + "columnsFrom": [ + "solar_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": { + "document_type": { + "name": "document_type", + "values": { + "EPR": "EPR", + "Condition Report": "Condition Report", + "Evidence Report": "Evidence Report", + "Summary Information": "Summary Information", + "Floor Plan": "Floor Plan", + "Scenario Draft EPC": "Scenario Draft EPC", + "Scenario Site Notes": "Scenario Site Notes" + } + }, + "cost_unit": { + "name": "cost_unit", + "values": { + "gbp_sq_meter": "gbp_sq_meter", + "gbp_per_unit": "gbp_per_unit", + "gbp_per_m2": "gbp_per_m2", + "gbp_per_m": "gbp_per_m" + } + }, + "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", + "cavity_wall_insulation": "cavity_wall_insulation", + "mechanical_ventilation": "mechanical_ventilation", + "loft_insulation": "loft_insulation", + "exposed_floor_insulation": "exposed_floor_insulation", + "flat_roof_insulation": "flat_roof_insulation", + "room_roof_insulation": "room_roof_insulation", + "cavity_wall_extraction": "cavity_wall_extraction", + "iwi_wall_demolition": "iwi_wall_demolition", + "iwi_vapour_barrier": "iwi_vapour_barrier", + "iwi_redecoration": "iwi_redecoration", + "suspended_floor_demolition": "suspended_floor_demolition", + "suspended_floor_redecoration": "suspended_floor_redecoration", + "suspended_floor_vapour_barrier": "suspended_floor_vapour_barrier", + "solid_floor_demolition": "solid_floor_demolition", + "solid_floor_preparation": "solid_floor_preparation", + "solid_floor_vapour_barrier": "solid_floor_vapour_barrier", + "solid_floor_redecoration": "solid_floor_redecoration", + "ewi_wall_demolition": "ewi_wall_demolition", + "ewi_wall_preparation": "ewi_wall_preparation", + "ewi_wall_redecoration": "ewi_wall_redecoration", + "low_energy_lighting_installation": "low_energy_lighting_installation", + "flat_roof_preparation": "flat_roof_preparation", + "flat_roof_vapour_barrier": "flat_roof_vapour_barrier", + "flat_roof_waterproofing": "flat_roof_waterproofing", + "windows_glazing": "windows_glazing" + } + }, + "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", + "survey": "survey", + "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" + } + }, + "housing_type": { + "name": "housing_type", + "values": { + "Private": "Private", + "Social": "Social" + } + }, + "unit_quantity": { + "name": "unit_quantity", + "values": { + "m2": "m2", + "part": "part" + } + }, + "scenario_type": { + "name": "scenario_type", + "values": { + "unit": "unit", + "building": "building" + } + } + }, + "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 3195bd0..089797c 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -708,6 +708,13 @@ "when": 1749810028140, "tag": "0100_wakeful_doctor_doom", "breakpoints": true + }, + { + "idx": 101, + "version": "5", + "when": 1752931250899, + "tag": "0101_bent_stature", + "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 1a21ac4..a2dc345 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -109,6 +109,7 @@ export const scenario = pgTable("scenario", { housingType: housingTypeEnum("housing_type").notNull(), goal: goalEnum("goal").notNull(), goalValue: text("goal_value"), + ashpCop: real("ashp_cop").default(2.8), // Coefficient of Performance for Air Source Heat Pumps defaults to 2.8 triggerFilePath: text("trigger_file_path"), alreadyInstalledFilePath: text("already_installed_file_path"), patchesFilePath: text("patches_file_path"), diff --git a/src/app/portfolio/[slug]/components/FormSchema.tsx b/src/app/portfolio/[slug]/components/FormSchema.tsx new file mode 100644 index 0000000..fbf3a3a --- /dev/null +++ b/src/app/portfolio/[slug]/components/FormSchema.tsx @@ -0,0 +1,44 @@ +// formSchemas.ts +import * as z from "zod"; +import { MeasureKeyEnum } from "@/app/db/schema/recommendations"; + +export const baseFormSchema = z.object({ + measures: z.array(MeasureKeyEnum).min(1, "At least one measure is required"), +}); + +export type BaseFormValues = z.infer; + +export const RemoteAssessmentFormSchema = baseFormSchema.extend({ + scenario: z.string().min(1), + goal: z.string().min(1), + goalValue: z.string().min(1), + housingType: z.string().min(1), + addressLineOne: z.string().min(1), + postcode: z.string().min(1), + uprn: z.number().min(1, "UPRN must be a valid number"), + valuation: z.number().min(1, "Valuation must be a valid number"), + propertyType: z.string().nullable(), + builtForm: z.string().nullable(), +}); + +export type RemoteAssessmentFormValues = z.infer< + typeof RemoteAssessmentFormSchema +>; + +export const uploadCsvSchema = baseFormSchema.extend({ + scenario: z.string().min(1), + goal: z.string().min(1), + goalValue: z.string().min(1), + housingType: z.string(), + ashpCop: z.preprocess((val) => { + if (val === "" || val === undefined) return undefined; + return Number(val); + }, z.number().min(0.1)), + budget: z.preprocess((val) => { + if (val === "" || val === undefined) return undefined; + if (val === null) return null; + return Number(val); + }, z.union([z.number(), z.null()]).optional()), +}); + +export type UploadCsvFormValues = z.infer; diff --git a/src/app/portfolio/[slug]/components/InputFile.tsx b/src/app/portfolio/[slug]/components/InputFile.tsx index 49e3b20..80ff585 100644 --- a/src/app/portfolio/[slug]/components/InputFile.tsx +++ b/src/app/portfolio/[slug]/components/InputFile.tsx @@ -28,7 +28,6 @@ export function InputFile({ return (
- = { + form: UseFormReturn; + name?: Path; // Optional override for custom field key +}; + +function MeasuresCheckboxes({ + form, + name = "measures" as Path, +}: MeasuresCheckboxesProps) { + const { control, setValue } = form; + + return ( +
+
+ + +
+ +
+ {measuresList.map((measure) => ( + ( + + { + const checked = e.target.checked; + const current = new Set(field.value ?? []); + checked ? current.add(measure) : current.delete(measure); + field.onChange(Array.from(current)); + }} + className="h-4 w-4" + /> + + {measuresDisplayLabels[measure]} + + + )} + /> + ))} +
+
+ ); +} + +export default MeasuresCheckboxes; diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index f50ad99..e1b5702 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -1,14 +1,13 @@ "use client"; -import { Dialog, Transition, Menu } from "@headlessui/react"; +import { Dialog, Transition } from "@headlessui/react"; import { Fragment, useMemo } from "react"; import { Input } from "@/app/shadcn_components/ui/input"; import { Button } from "@/app/shadcn_components/ui/button"; import { useMutation } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; -import { useForm, FormProvider, useFormContext } from "react-hook-form"; +import { useForm, FormProvider } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import * as z from "zod"; import { FormField, FormItem, @@ -24,11 +23,12 @@ import { SelectScenarioDropdown, SelectDropdown, } from "./RemoteAssessmentDropdowns"; +import MeasuresCheckboxes from "./MeasuresCheckboxes"; +import { measuresList } from "@/app/db/schema/recommendations"; import { - measuresList, - measuresDisplayLabels, - MeasureKeyEnum, -} from "@/app/db/schema/recommendations"; + RemoteAssessmentFormSchema, + RemoteAssessmentFormValues, +} from "./FormSchema"; type Option = { label: string; @@ -161,22 +161,6 @@ interface EngineTriggerBody { scenario_id?: string | null; } -const formSchema = z.object({ - scenario: z.string().min(1), - goal: z.string().min(1), - goalValue: z.string().min(1), - housingType: z.string().min(1), - addressLineOne: z.string().min(1), - postcode: z.string().min(1), - uprn: z.number().min(1), - valuation: z.number().min(1), - propertyType: z.string().nullable(), - builtForm: z.string().nullable(), - measures: z.array(MeasureKeyEnum).min(1, "Select at least one measure."), -}); - -type FormValues = z.infer; - async function uploadCsvToS3({ presignedUrl, file, @@ -280,10 +264,10 @@ function useCreateRemoteAssessment({ scenarioId, }: { portfolioId: string; - uprn: number | null; + uprn: number | undefined | null; addressLineOne: string; postcode: string; - valuation: string | number | null; + valuation: number | undefined | null; measures: (typeof measuresList)[number][]; propertyType?: string | null; builtForm?: string | null; @@ -298,6 +282,10 @@ function useCreateRemoteAssessment({ const session = useSession(); const userId = String(session.data?.user.dbId); + if (uprn === undefined || valuation === undefined) { + throw new Error("UPRN and valuation must be provided"); + } + const { assetListFileKey, valuationDataFileKey } = useMemo( () => generateS3Keys(userId, portfolioId), [userId, portfolioId] @@ -367,7 +355,7 @@ function useCreateRemoteAssessment({ }, }); - async function triggerEngine(data: FormValues) { + async function triggerEngine(data: RemoteAssessmentFormValues) { try { const triggerBody: EngineTriggerBody = { scenario_id: scenarioId === "__new__" ? null : scenarioId, @@ -406,7 +394,7 @@ function useCreateRemoteAssessment({ } } - async function handleSubmit(formData: FormValues) { + async function handleSubmit(formData: RemoteAssessmentFormValues) { try { await Promise.all([ mutatePresignedUrl({ @@ -465,10 +453,8 @@ export default function RemoteAssessmentModal({ [scenarios] ); - console.log("Scenario options:", scenarioOptions); - - const form = useForm({ - resolver: zodResolver(formSchema), + const form = useForm({ + resolver: zodResolver(RemoteAssessmentFormSchema), mode: "onChange", defaultValues: { scenario: "", @@ -477,10 +463,10 @@ export default function RemoteAssessmentModal({ goalValue: "", addressLineOne: "", postcode: "", - uprn: 0, - valuation: 0, - propertyType: null as string | null, - builtForm: null as string | null, + uprn: undefined, // Must match expected type + valuation: undefined, // Must match expected type + propertyType: null, + builtForm: null, measures: measuresList, }, }); @@ -495,10 +481,10 @@ export default function RemoteAssessmentModal({ presignedUrlIsError, } = useCreateRemoteAssessment({ portfolioId, - uprn: form.watch("uprn"), + uprn: form.watch("uprn") ?? null, addressLineOne: form.watch("addressLineOne"), postcode: form.watch("postcode"), - valuation: form.watch("valuation"), + valuation: form.watch("valuation") ?? null, propertyType: form.watch("propertyType"), builtForm: form.watch("builtForm"), measures: measures, @@ -750,8 +736,13 @@ export default function RemoteAssessmentModal({ type="number" placeholder="Enter UPRN" {...field} + value={field.value ?? ""} onChange={(e) => - field.onChange(Number(e.target.value)) + field.onChange( + e.target.value === "" + ? undefined + : Number(e.target.value) + ) } className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown" /> @@ -786,8 +777,13 @@ export default function RemoteAssessmentModal({ type="number" placeholder="Enter valuation" {...field} + value={field.value ?? ""} onChange={(e) => - field.onChange(Number(e.target.value)) + field.onChange( + e.target.value === "" + ? undefined + : Number(e.target.value) + ) } className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown" /> @@ -888,70 +884,3 @@ export default function RemoteAssessmentModal({ function setIsOpen(arg0: boolean) { throw new Error("Function not implemented."); } - -function MeasuresCheckboxes({ - form, -}: { - form: ReturnType>; -}) { - const { control } = form; - const allMeasures = measuresList; - return ( -
-
- - -
- - {/* Measures grid */} -
- {measuresList.map((measure) => ( - ( - - { - const checked = e.target.checked; - const current = new Set(field.value ?? []); - if (checked) { - current.add(measure); - } else { - current.delete(measure); - } - field.onChange(Array.from(current)); - }} - className="h-4 w-4" - /> - - {measuresDisplayLabels[measure]} - - - )} - /> - ))} -
-
- ); -} diff --git a/src/app/portfolio/[slug]/components/SubmitPlan.tsx b/src/app/portfolio/[slug]/components/SubmitPlan.tsx deleted file mode 100644 index 06b2339..0000000 --- a/src/app/portfolio/[slug]/components/SubmitPlan.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useMutation } from "@tanstack/react-query"; -import { useSession } from "next-auth/react"; -import { useMemo } from "react"; - -function generateS3Key(userId: string, portfolioId: string, filename: string) { - const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); - const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`; - return key; -} - -interface UseCreatePlanProps { - portfolioId: string; - housingType: string; - goal: string; - goalValue: string; - file: File; -} - -interface UseCreatePlanReturn { - handlePlanBuild: () => void; - isGeneratingUrlLoading: boolean; - isUploadLoading: boolean; -} - -const useCreatePlan = ({ - portfolioId, - housingType, - goal, - goalValue, - file, -}: UseCreatePlanProps): UseCreatePlanReturn => { - const router = useRouter(); - const session = useSession(); - - // Since userId is a big int it gets coerced into a string - const userId = String(session.data?.user.dbId); - - // Every time the component is re-rendered, a new file key will be generated. To prevent this, - // we use useMemo to only generate a new file key when the userId or portfolioId changes. - const fileKey = useMemo( - () => generateS3Key(userId, portfolioId, "portfolio_plan_properties"), - [userId, portfolioId] - ); - - const { mutate: mutateUploadCsv, isLoading: isUploadLoading } = useMutation( - uploadCsvToS3, - { - onSuccess: () => { - const body = JSON.stringify({ - portfolio_id: portfolioId, - housing_type: housingType, - goal: goal, - goal_value: goalValue, - trigger_file_path: fileKey, - }); - - const response = fetch(`/api/plan/trigger`, { - method: "POST", - body: body, - }); - - return response; - }, - onError: (error) => { - console.error(error); - }, - } - ); - - const { mutate, isLoading: isGeneratingUrlLoading } = useMutation( - generatePresignedUrl, - { - onSuccess: (data) => { - try { - const response = mutateUploadCsv({ - presignedUrl: data.url, - file: file, - }); - - return response; - } catch (error) { - console.error(error); - } - }, - onError: (error) => { - console.error(error); - }, - } - ); - - const handlePlanBuild = () => { - mutate({ userId: userId, portfolioId: portfolioId, fileKey: fileKey }); - router.push(`/portfolio/${portfolioId}/plan-loading`); - }; - - return { - handlePlanBuild, - isGeneratingUrlLoading, - isUploadLoading, - }; -}; - -async function generatePresignedUrl({ - userId, - portfolioId, - fileKey, -}: { - userId: string; - portfolioId: string; - fileKey: string; -}) { - const body = JSON.stringify({ - userId: userId, - portfolioId: portfolioId, - fileKey: fileKey, - }); - - const presignedResponse = await fetch(`/api/upload/csv`, { - method: "POST", - body: body, - }); - if (!presignedResponse.ok) { - throw new Error("Network response was not ok"); - } - const presignedUrl = await presignedResponse.json(); - return presignedUrl; -} - -async function uploadCsvToS3({ - presignedUrl, - file, -}: { - presignedUrl: string; - file: File; -}) { - try { - const response = await fetch(presignedUrl, { - method: "PUT", - body: file, - headers: { "Content-Type": "text/csv" }, - }); - - if (!response.ok) { - console.error(response); - throw new Error("Network response was not ok"); - } - } catch (error) { - console.error(error); - throw new Error("Upload failed."); - } - - return { success: true }; -} - -export const SubmitPlan = ({ - buttonDisabled, - goal, - housingType, - goalValue, - file, - portfolioId, -}: { - buttonDisabled: boolean; - goal: string; - housingType: string; - goalValue: string; - file: File; - portfolioId: string; -}) => { - const { handlePlanBuild, isGeneratingUrlLoading, isUploadLoading } = - useCreatePlan({ portfolioId, housingType, goal, goalValue, file }); - - let buttonText; - if (isUploadLoading) { - buttonText = "Uploading file..."; - } else if (isGeneratingUrlLoading) { - buttonText = "Creating plan..."; - } else { - buttonText = "Create"; - } - - return ( - - ); -}; diff --git a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx index 0e3c7c1..bf11fdb 100644 --- a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx +++ b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx @@ -1,205 +1,316 @@ "use client"; -import { Menu, Dialog, Transition } from "@headlessui/react"; -import { Fragment, useState } from "react"; -import { ChevronDownIcon } from "@heroicons/react/20/solid"; -import { Float } from "@headlessui-float/react"; -import { InputFile } from "@/app/portfolio/[slug]/components/InputFile"; -import { SubmitPlan } from "@/app/portfolio/[slug]/components/SubmitPlan"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useMemo, useState } from "react"; import { useMutation } from "@tanstack/react-query"; +import { InputFile } from "@/app/portfolio/[slug]/components/InputFile"; +import { ScenarioSelect } from "@/app/db/schema/recommendations"; +import MeasuresCheckboxes from "./MeasuresCheckboxes"; +import { useForm, FormProvider } from "react-hook-form"; +import { + SelectDropdown, + SelectScenarioDropdown, +} from "@/app/portfolio/[slug]/components/RemoteAssessmentDropdowns"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/app/shadcn_components/ui/form"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { measuresList } from "@/app/db/schema/recommendations"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + uploadCsvSchema, + UploadCsvFormValues, +} from "@/app/portfolio/[slug]/components/FormSchema"; -type Option = { - label: string; - value: string; - disabled: boolean; -}; - -type DropdownProps = { - options: Option[]; - selectedOption: string; - onSelectOption: (option: Option) => void; -}; - -export function SelectDropdown({ - options, - selectedOption, - onSelectOption, -}: DropdownProps) { - return ( - - - - {selectedOption || "Select an option"} - - - - {options.map((option) => ( - - {({ active }) => ( - - )} - - ))} - - - - - ); -} - -const hiddenInputArrows = - "[-moz-appearance:_textfield] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none"; +const NEW_SENTINEL = "__new__"; const selecthousingTypeOptions = [ - { - label: "Social", - value: "Social", - disabled: false, - }, - { - label: "Private", - value: "Private", - disabled: false, - }, + { label: "Social", value: "Social", disabled: false }, + { label: "Private", value: "Private", disabled: false }, ]; const selectGoalOptions = [ + { label: "Increasing EPC", value: "Increasing EPC", disabled: false }, // { - // label: "None", - // value: "None", + // label: "Reduce energy consumption", + // value: "Reduce energy consumption", + // disabled: false, // }, - { - label: "Increase EPC", - value: "Increase EPC", - disabled: false, - }, - { - label: "Reduce energy consumption", - value: "Reduce energy consumption", - disabled: false, - }, ]; const goalValueOptions = [ - { - label: "C", - value: "C", - disabled: false, - }, - { - label: "B", - value: "B", - disabled: false, - }, - { - label: "A", - value: "A", - disabled: false, - }, + { label: "C", value: "C", disabled: false }, + { label: "B", value: "B", disabled: false }, + { label: "A", value: "A", disabled: false }, ]; +function generateS3Key( + userId: string, + portfolioId: string, + fileType: "csv" | "xlsx" +) { + const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); + return `${userId}/${portfolioId}/${timestamp}/asset_list.${fileType}`; +} + +async function uploadCsvToS3({ + presignedUrl, + file, +}: { + presignedUrl: string; + file: File; +}) { + const response = await fetch(presignedUrl, { + method: "PUT", + body: file, + headers: { "Content-Type": "text/csv" }, + }); + + if (!response.ok) throw new Error("Upload failed"); + return { success: true }; +} + +async function generatePresignedUrl({ + userId, + portfolioId, + fileKey, +}: { + userId: string; + portfolioId: string; + fileKey: string; +}) { + const response = await fetch("/api/upload/csv", { + method: "POST", + body: JSON.stringify({ userId, portfolioId, fileKey }), + }); + + if (!response.ok) throw new Error("Failed to generate presigned URL"); + const data = await response.json(); + return { url: data.url, fileKey }; +} +export function useUploadCsvPlan({ + file, + portfolioId, + goal, + goalValue, + housingType, + scenarioName, + selectedSheet, + budget, + ashpCop, + measures, + onSuccessRedirect, + fileType, +}: { + file: File; + portfolioId: string; + goal: string; + goalValue: string; + budget: number | null; + ashpCop: number; + housingType: string; + scenarioName: string; + selectedSheet: string; + measures: (typeof measuresList)[number][]; + onSuccessRedirect: (path: string) => void; + fileType: "csv" | "xlsx"; +}) { + const session = useSession(); + const userId = String(session.data?.user.dbId); + + const fileKey = useMemo( + () => generateS3Key(userId, portfolioId, fileType), + [userId, portfolioId, fileType] + ); + + const { mutateAsync: uploadFileToS3, isLoading: isUploadLoading } = + useMutation(uploadCsvToS3); + + const { mutateAsync: getPresignedUrl, isLoading: isGeneratingUrlLoading } = + useMutation(generatePresignedUrl); + + const handleSubmit = async () => { + try { + const { url } = await getPresignedUrl({ userId, portfolioId, fileKey }); + + await uploadFileToS3({ presignedUrl: url, file }); + + const body = { + scenario_id: scenarioName === NEW_SENTINEL ? null : scenarioName, + portfolio_id: portfolioId, + housing_type: housingType, + goal: goal, + goal_value: goalValue, + trigger_file_path: fileKey, + valuation_file_path: "", // Not used in this context + already_installed_file_path: "", + patches_file_path: "", + non_invasive_recommendations_file_path: "", + multi_plan: true, + budget: budget, + scenario_name: scenarioName, + inclusions: measures, + event_type: "remote_assessment", + sheet_name: selectedSheet, + ashp_cop: ashpCop, + file_type: fileType, // Pass the file type for backend processing + }; + + const triggerRes = await fetch("/api/plan/trigger", { + method: "POST", + body: JSON.stringify(body), + }); + + if (!triggerRes.ok) throw new Error("Failed to trigger engine"); + + onSuccessRedirect(`/portfolio/${portfolioId}/plan-loading`); + } catch (err) { + console.error("Upload or trigger failed", err); + throw err; + } + }; + + return { + handleSubmit, + isUploadLoading, + isGeneratingUrlLoading, + }; +} + export default function UploadCsvModal({ isOpen = false, setIsOpen, portfolioId, + scenarios, }: { isOpen?: boolean; - setIsOpen: (isOpen: boolean) => void; + setIsOpen: (open: boolean) => void; portfolioId: string; + scenarios: ScenarioSelect[]; }) { - const [budget, setBudget] = useState(undefined); + const form = useForm({ + resolver: zodResolver(uploadCsvSchema), + shouldUnregister: false, + mode: "onChange", + defaultValues: { + scenario: "", + housingType: "", + goal: "", + goalValue: "", + budget: null, + measures: measuresList, + }, + }); + const { formState } = form; + const { isValid, isSubmitting } = formState; - const [buttonDisabled, setButtonDisabled] = useState(true); - const [selectedGoal, setSelectedGoal] = useState(""); - const [housingType, sethousingType] = useState(""); - const [goalValue, setGoalValue] = useState(""); + const router = useRouter(); + const [sheetNames, setSheetNames] = useState([]); + const [selectedSheet, setSelectedSheet] = useState(""); const [csvFile, setCsvFile] = useState(null); + const [selectedScenario, setSelectedScenario] = useState(null); + const [showMeasures, setShowMeasures] = useState(false); + const [fileType, setFileType] = useState<"csv" | "xlsx">("csv"); - function handleBudgeChange(e: React.ChangeEvent) { - setBudget(e.target.valueAsNumber); - } + const scenarioOptions = useMemo( + () => + scenarios.map((s) => ({ + label: s.name || "", + value: String(s.id) || "", + disabled: false, + })), + [scenarios] + ); - function handleButtonDisabled( - goal?: string, - housetype?: string, - value?: string, - file?: File | null - ) { - // This function is defined as such to accomodate for the asynchonous nature of state setting - // The first time this is called, the setState function will be run before this but the state value - // will not have updated yet, so we need to pass in the value as an argument to check if the value has been updated - if ( - (goal || selectedGoal) && - (housetype || housingType) && - (value || goalValue) && - (file || csvFile) - ) { - setButtonDisabled(false); - } - } - - const { - mutate: validateFile, - isLoading: isValidating, - error: validationError, - } = useMutation( + const { mutate: validateFile, isLoading: isValidating } = useMutation( async (file: File) => { const formData = new FormData(); formData.append("file", file); - const response = await fetch("/api/upload/validate", { method: "POST", body: formData, }); - - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || "File validation failed"); - } - + if (!response.ok) throw new Error("File validation failed"); return response.json(); }, { - onSuccess: () => { - setButtonDisabled(false); + onSuccess: (data) => { + if (data.sheetNames) { + setSheetNames(data.sheetNames); + setSelectedSheet(data.sheetNames[0] || ""); + } else { + setSheetNames([]); + setSelectedSheet(""); + } + setFileType(data.file_type); // capture file type }, - onError: (err) => { - console.error(err); - setButtonDisabled(true); + onError: () => { + setSheetNames([]); + setSelectedSheet(""); }, } ); + const onSelectScenario = (opt: { value: string }) => { + setSelectedScenario(opt.value); + if (opt.value === NEW_SENTINEL) { + form.reset({ + ...form.getValues(), + scenario: "", + housingType: "", + goal: "", + goalValue: "", + ashpCop: 2.8, // Default COP value - eventually to come from db + }); + } else { + const picked = scenarios.find((s) => String(s.id) === opt.value); + if (!picked) return; + form.setValue("scenario", picked.name || ""); + form.setValue("housingType", picked.housingType); + form.setValue("goal", picked.goal); + form.setValue("goalValue", picked.goalValue || ""); + form.setValue("ashpCop", picked.ashpCop || 2.8); // Default COP value + form.setValue("budget", picked.budget || null); + } + }; + + const uploadCsvPlan = useUploadCsvPlan({ + file: csvFile!, + goal: form.watch("goal"), + goalValue: form.watch("goalValue"), + housingType: form.watch("housingType"), + budget: form.watch("budget") || null, + ashpCop: form.watch("ashpCop"), + scenarioName: form.watch("scenario"), + measures: form.watch("measures"), + fileType: fileType, + portfolioId, + selectedSheet, + onSuccessRedirect: (path) => router.push(path), + }); + + const onSubmit = form.handleSubmit(async () => { + if (!csvFile || !selectedSheet) return; + await uploadCsvPlan.handleSubmit(); + form.reset(); + }); + return ( - <> - - setIsOpen(false)} - > + + setIsOpen(false)} + > +
-
+ - -
-
- - - - Upload Properties - - -
- Upload a csv of properties and input some details about how - you want to retrofit your properties + + +
+ + Upload Property Data + + +
+ {/* File Upload */} +
+ { + setCsvFile(file); + validateFile(file); + }} + isValidating={isValidating} + /> + + View example CSV format +
-
- -

- If you don't set a budget, we will aim to minimise - cost anyway -

-
- £ - handleBudgeChange(e)} - onKeyDown={(e) => - (e.key === "e" || e.key === "E") && e.preventDefault() - } - className="p-2 focus:outline-none bg-transparent w-full" + {/* Sheet selection */} + {sheetNames.length > 0 && ( +
+ + ({ + label: name, + value: name, + }))} + selectedOption={selectedSheet} + onSelectOption={(opt) => setSelectedSheet(opt.value)} /> +

+ Ensure this sheet uses the Standardised Asset List + format. +

-
+ )} -
- - { - sethousingType(option.value); - handleButtonDisabled( - selectedGoal, - option.value, - goalValue, - csvFile - ); - }} - /> -
+ {/* Scenario selection */} + + Select scenario + + + + -
- - { - setSelectedGoal(option.value); - handleButtonDisabled( - option.value, - housingType, - goalValue, - csvFile - ); - }} - /> - {selectedGoal === "Increase EPC" && ( -
- - { - setGoalValue(option.value); - handleButtonDisabled( - selectedGoal, - housingType, - option.value, - csvFile - ); - }} + {/* Scenario definition inputs */} + {selectedScenario !== null && ( + <> +
+ {/* Scenario Name */} + ( + + + Scenario Name + + + + + + + )} + /> + + {/* Housing Type */} + ( + + + Housing Type + + + {selectedScenario === NEW_SENTINEL ? ( + + field.onChange(o.value) + } + /> + ) : ( + + )} + + + + )} />
- )} -
-
-
- Download the example csv and fill in the details for your - properties -
- -
+
+ {/* Goal */} + ( + + + Goal + + + {selectedScenario === NEW_SENTINEL ? ( + + field.onChange(o.value) + } + /> + ) : ( + + )} + + + + )} + /> -
-
- validateFile(file)} - isValidating={isValidating} - /> -
-
+ {/* Goal Value */} + ( + + + Goal Value + + + {selectedScenario === NEW_SENTINEL ? ( + + field.onChange(opt.value) + } + /> + ) : ( + + )} + + + + )} + /> +
-
+
+ {/* Heat Pump COP */} + ( + + + Heat Pump COP + + + + + + + )} + /> + + {/* Budget */} + ( + + + Budget (£) + + + + + + + )} + /> +
+ + {/* ( + + Heat pump COP + + + + + )} + /> */} + + {/* ( + + Budget (optional) + + + field.onChange( + e.target.value === "" + ? null + : parseFloat(e.target.value) + ) + } + /> + + + )} + /> */} + + )} + + {/* Measures */} +
+
+ +
+
+ +
+ - + +
- - + +
-
-
-
- + +
+ + ); }