mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge pull request #143 from Hestia-Homes/new-reporting
Migrated database to make epc store data non-mandatory
This commit is contained in:
commit
10642d236a
17 changed files with 23592 additions and 10 deletions
17
src/app/db/migrations/0128_thin_wiccan.sql
Normal file
17
src/app/db/migrations/0128_thin_wiccan.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
CREATE TABLE "epc_store" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"uprn" bigint,
|
||||
"epc_api_created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"epc_api" jsonb NOT NULL,
|
||||
"epc_page_created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"epc_page" text NOT NULL,
|
||||
"epc_page_rrn" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "property_installed_measures" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"uprn" bigint NOT NULL,
|
||||
"measure_type" "type" NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"installed_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
4
src/app/db/migrations/0129_workable_medusa.sql
Normal file
4
src/app/db/migrations/0129_workable_medusa.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE "epc_store" ALTER COLUMN "epc_api_created_at" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "epc_store" ALTER COLUMN "epc_api_created_at" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "epc_store" ALTER COLUMN "epc_page_created_at" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "epc_store" ALTER COLUMN "epc_page_created_at" DROP NOT NULL;
|
||||
3
src/app/db/migrations/0130_large_wong.sql
Normal file
3
src/app/db/migrations/0130_large_wong.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE "epc_store" ALTER COLUMN "epc_api" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "epc_store" ALTER COLUMN "epc_page" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "epc_store" ALTER COLUMN "epc_page_rrn" DROP NOT NULL;
|
||||
1
src/app/db/migrations/0131_minor_lucky_pierre.sql
Normal file
1
src/app/db/migrations/0131_minor_lucky_pierre.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TYPE "public"."type" ADD VALUE 'double_glazing' BEFORE 'trickle_vent';
|
||||
1
src/app/db/migrations/0132_cynical_ikaris.sql
Normal file
1
src/app/db/migrations/0132_cynical_ikaris.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "property_details_epc" ADD COLUMN "sap_05_overwritten" boolean DEFAULT false;
|
||||
4690
src/app/db/migrations/meta/0128_snapshot.json
Normal file
4690
src/app/db/migrations/meta/0128_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
4688
src/app/db/migrations/meta/0129_snapshot.json
Normal file
4688
src/app/db/migrations/meta/0129_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
4688
src/app/db/migrations/meta/0130_snapshot.json
Normal file
4688
src/app/db/migrations/meta/0130_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
4689
src/app/db/migrations/meta/0131_snapshot.json
Normal file
4689
src/app/db/migrations/meta/0131_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
4696
src/app/db/migrations/meta/0132_snapshot.json
Normal file
4696
src/app/db/migrations/meta/0132_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -897,6 +897,41 @@
|
|||
"when": 1763119819059,
|
||||
"tag": "0127_acoustic_sleepwalker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 128,
|
||||
"version": "7",
|
||||
"when": 1764072359053,
|
||||
"tag": "0128_thin_wiccan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 129,
|
||||
"version": "7",
|
||||
"when": 1764077820379,
|
||||
"tag": "0129_workable_medusa",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 130,
|
||||
"version": "7",
|
||||
"when": 1764077914945,
|
||||
"tag": "0130_large_wong",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 131,
|
||||
"version": "7",
|
||||
"when": 1764191284790,
|
||||
"tag": "0131_minor_lucky_pierre",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 132,
|
||||
"version": "7",
|
||||
"when": 1764401779805,
|
||||
"tag": "0132_cynical_ikaris",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
44
src/app/db/schema/epc.ts
Normal file
44
src/app/db/schema/epc.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
text,
|
||||
jsonb,
|
||||
timestamp,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
// This table stores postcode search results from the OS Places API
|
||||
// for re-use and caching purposes. The data is stored in jsonb format, to
|
||||
// allow for fast queries and flexibility with the API response structure.
|
||||
|
||||
export interface OSPlacesHeader {
|
||||
totalresults?: number;
|
||||
offset?: number;
|
||||
maxresults?: number;
|
||||
[k: string]: any;
|
||||
}
|
||||
|
||||
export interface EpcApiResponse {
|
||||
header?: OSPlacesHeader;
|
||||
rows?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const epcStore = pgTable("epc_store", {
|
||||
id: serial("id").primaryKey(),
|
||||
uprn: bigint("uprn", { mode: "bigint" }),
|
||||
|
||||
// Timestamp for when the EPC API entry was first stored
|
||||
epcApiCreatedAt: timestamp("epc_api_created_at"),
|
||||
|
||||
// EPC API response for the UPRN
|
||||
epcApi: jsonb("epc_api").$type<EpcApiResponse>(),
|
||||
|
||||
// Timestamp for when the EPC page was stored
|
||||
epcPageCreatedAt: timestamp("epc_page_created_at"),
|
||||
|
||||
// HTML content of the EPC page
|
||||
epcPage: text("epc_page"),
|
||||
|
||||
// RRN of the EPC page
|
||||
epcPageRrn: text("epc_page_rrn"),
|
||||
});
|
||||
|
|
@ -43,6 +43,7 @@ export const MaterialType: [string, ...string[]] = [
|
|||
// Windows
|
||||
"windows_glazing",
|
||||
"secondary_glazing",
|
||||
"double_glazing",
|
||||
// vents
|
||||
"trickle_vent",
|
||||
"door_undercut",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "drizzle-orm/pg-core";
|
||||
import { portfolio, PortfolioStatus } from "./portfolio";
|
||||
import { InferModel } from "drizzle-orm";
|
||||
import { materialTypeEnum } from "./materials";
|
||||
|
||||
// This is a placeholder for the property schema
|
||||
export interface PropertyMeta {
|
||||
|
|
@ -177,6 +178,9 @@ export const propertyDetailsEpc = pgTable("property_details_epc", {
|
|||
"current_energy_demand_heating_hotwater"
|
||||
),
|
||||
estimated: boolean("estimated").default(false),
|
||||
// We indicate if the property has an overwritten SAP 05 EPC. I.e. there is a valid EPC, however it's a SAP 05
|
||||
// EPC which isn't particularly useful. This value is defaulted to False
|
||||
sap05Overwritten: boolean("sap_05_overwritten").default(false),
|
||||
// Include current estimates for energy bills, across the different types of energy
|
||||
// These predictions are based on the EPC predicted consumptions + current energy prices
|
||||
heatingEnergyCostCurrent: real("heating_cost_current"),
|
||||
|
|
@ -241,6 +245,23 @@ export const nonIntrusiveSurveyNotes = pgTable("non_intrusive_survey_notes", {
|
|||
note: text("note").notNull(),
|
||||
});
|
||||
|
||||
// This model is a record of the measures that have already been installed for a property
|
||||
// This is considered as supplementary daa and stored against the UPRN
|
||||
// RecommendationType is the
|
||||
export const propertyInstalledMeasures = pgTable(
|
||||
"property_installed_measures",
|
||||
{
|
||||
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
||||
uprn: bigint("uprn", { mode: "bigint" }).notNull(),
|
||||
// The material types define the list of measures we should expect
|
||||
measureType: materialTypeEnum("measure_type").notNull(),
|
||||
// Record of when this measure was inserted into the db
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
// Record of when this measure was actually installed
|
||||
installedAt: timestamp("installed_at").notNull().defaultNow(),
|
||||
}
|
||||
);
|
||||
|
||||
export type Property = InferModel<typeof property, "select">;
|
||||
export type PropertyDetailsEpc = InferModel<
|
||||
typeof propertyDetailsEpc,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ export function InputFile({
|
|||
onFileSelect,
|
||||
isValidating,
|
||||
isValid,
|
||||
errorMessage,
|
||||
}: {
|
||||
onFileSelect: (file: File) => void;
|
||||
isValidating: boolean;
|
||||
isValid: boolean | null;
|
||||
errorMessage: string | null;
|
||||
}) {
|
||||
function handleOnChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
|
|
@ -18,14 +20,11 @@ export function InputFile({
|
|||
const validExtensions = ["csv", "xls", "xlsx"];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
|
||||
console.log("Extension: ", fileExtension);
|
||||
|
||||
if (!fileExtension || !validExtensions.includes(fileExtension)) {
|
||||
console.error("Unsupported file type");
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
console.log("TRIGGER");
|
||||
|
||||
onFileSelect(file);
|
||||
}
|
||||
|
|
@ -46,9 +45,7 @@ export function InputFile({
|
|||
)}
|
||||
|
||||
{isValid === false && !isValidating && (
|
||||
<span className="text-sm text-red-500 mt-1">
|
||||
File validation failed
|
||||
</span>
|
||||
<span className="text-sm text-red-500 mt-1">{errorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function SelectScenarioDropdown({
|
|||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10 py-1">
|
||||
<Menu.Items className="absolute mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10 py-1 overflow-auto max-h-60 scroll-py-1">
|
||||
{options.map((opt) => (
|
||||
<Menu.Item key={opt.value}>
|
||||
{({ active }) => (
|
||||
|
|
|
|||
|
|
@ -168,12 +168,13 @@ export async function validateClientFile(file: File): Promise<{
|
|||
sheetRowCounts[sheetName] = rowCount;
|
||||
}
|
||||
|
||||
console.log("Sheet Counts", sheetRowCounts);
|
||||
|
||||
if (!isStandardised) {
|
||||
// Explain why the file is not standardised
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
error: "Excel file is not a valid domna asset list.",
|
||||
error:
|
||||
"Excel file is not a valid domna asset list - either no 'Standardised Asset List' tab or no domna property ID reference.",
|
||||
file_type: "xlsx",
|
||||
sheetNames,
|
||||
sheetRowCounts,
|
||||
|
|
@ -369,6 +370,7 @@ export default function UploadCsvModal({
|
|||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [fileIsValid, setFileIsValid] = useState<boolean | null>(null);
|
||||
const [sheetCounts, setSheetCounts] = useState<Record<string, number>>({});
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const scenarioOptions = useMemo(
|
||||
() =>
|
||||
|
|
@ -482,6 +484,10 @@ export default function UploadCsvModal({
|
|||
|
||||
if (!result.isValid) {
|
||||
setFileIsValid(false);
|
||||
// Set validation message
|
||||
setErrorMessage(
|
||||
result.error || "File validation failed"
|
||||
);
|
||||
setCsvFile(null);
|
||||
setSheetNames([]);
|
||||
setSelectedSheet("");
|
||||
|
|
@ -504,6 +510,7 @@ export default function UploadCsvModal({
|
|||
}}
|
||||
isValidating={isValidating}
|
||||
isValid={fileIsValid}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
|
||||
<a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue