Merge pull request #143 from Hestia-Homes/new-reporting

Migrated database to make epc store data non-mandatory
This commit is contained in:
KhalimCK 2025-11-29 15:39:06 +08:00 committed by GitHub
commit 10642d236a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 23592 additions and 10 deletions

View 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
);

View 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;

View 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;

View file

@ -0,0 +1 @@
ALTER TYPE "public"."type" ADD VALUE 'double_glazing' BEFORE 'trickle_vent';

View file

@ -0,0 +1 @@
ALTER TABLE "property_details_epc" ADD COLUMN "sap_05_overwritten" boolean DEFAULT false;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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
View 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"),
});

View file

@ -43,6 +43,7 @@ export const MaterialType: [string, ...string[]] = [
// Windows
"windows_glazing",
"secondary_glazing",
"double_glazing",
// vents
"trickle_vent",
"door_undercut",

View file

@ -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,

View file

@ -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>
);

View file

@ -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 }) => (

View file

@ -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