Merge pull request #62 from Hestia-Homes/feature/condition_report_etl

Feature/condition report etl
This commit is contained in:
Jun-te Kim 2025-06-17 09:54:38 +01:00 committed by GitHub
commit 188972ce64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2377 additions and 27 deletions

View file

@ -22,7 +22,8 @@
"lindacong.vscode-book-reader",
"4ops.terraform",
"fabiospampinato.vscode-todo-plus",
"jgclark.vscode-todo-highlight"
"jgclark.vscode-todo-highlight",
"corentinartaud.pdfpreview"
]
}
}

View file

@ -0,0 +1,6 @@
from etl.surveyedData.surveryedData import surveyedDataProcessor
condition_report_file_path = "/workspaces/survey-extractor/etl/files/osmosis_condition_report.pdf"
sdp = surveyedDataProcessor("123 Fake Street", [condition_report_file_path])
# TODO: add the ability to add document type, and sharepoint or s3 link so we can process access it again

View file

@ -63,6 +63,7 @@ for pipeline in pipelines.results:
for stage in pipeline.stages:
if stage.label.upper().strip() not in [s.upper() for s in exclude_stage.get(pipeline_name, [])]:
for deals in hubspot.get_all_deals_from_stage_id(stage.id):
print(f"Scraping deal {deals['deal_id']}")
time.sleep(1)
deal_notes_by_week = {"Week 1": [], "Week 2": [], "Week 3": []}
notes = hubspot.get_notes_from_deals_id(deals["deal_id"])
@ -71,7 +72,12 @@ for pipeline in pipelines.results:
week_label = get_week_label(note["created_at"])
if not week_label:
continue
html_body = note['note']
html_body = note.get('note')
if not html_body:
print(f"Skipping note with missing 'note' field: {note}")
continue
print(f"Debugging purposes html_body looks like {html_body}")
soup = BeautifulSoup(html_body, "html.parser")
plain_text = soup.get_text(separator="\n")
deal_notes_by_week[week_label].append(plain_text)
@ -85,11 +91,12 @@ for pipeline in pipelines.results:
except:
owner_name = "Couldn't find owner information"
# Unique identifier to Domna Homes' hubspot
portal_id = 145275138
notes_data[pipeline_name].append({
"Deal Name": deal_name.upper(),
"Deal URL": f"https://app-eu1.hubspot.com/contacts/{portal_id}/record/0-3/{deals["deal_id"]}/",
"Deal URL": f"https://app-eu1.hubspot.com/contacts/{portal_id}/record/0-3/{deals['deal_id']}/",
"Deal Owner": owner_name,
"Deal Stage": stage.label.upper(),
"Value": deals["value"],

Binary file not shown.

View file

@ -34,4 +34,5 @@ class Documents(BaseModel, table=True):
target_table: str
target_id: uuid.UUID
Documents.update_forward_refs()
Documents.update_forward_refs()

View file

@ -10,6 +10,8 @@ from etl.scraper.scraper import SharePointScraper
# board_id = "4965130190"
# liz green
board_id = "6097500103"
# Cocuun
# board_id = "5185076280"
monday_key = "eyJhbGciOiJIUzI1NiJ9.eyJ0aWQiOjQ5ODc2ODQxOCwiYWFpIjoxMSwidWlkIjozNjE3ODAzNCwiaWFkIjoiMjAyNS0wNC0xMVQxMToyMzoxNy40NjdaIiwicGVyIjoibWU6d3JpdGUiLCJhY3RpZCI6MTM5OTc4MjMsInJnbiI6InVzZTEifQ.-2Lit4s46ZF6AXuMW9t0TxIaFLkHqD4Yo-PyM9i2XZY"
monday = MondayClient(monday_key)
@ -24,6 +26,7 @@ parent_folder = "/Osmosis ACD/Osmosis ACD Projects/"
# Change this per installer
# parent_folder += "Stonewater/Wates/REDO"
parent_folder += "Installer Documentation/Platform Housing Group/Liv Green"
# parent_folder += "Installer Documentation/Platform Housing Group/Cocuun/REDO"
import re
@ -32,7 +35,7 @@ def sanitize_name(name: str, ignore_dot = False) -> str:
if ignore_dot:
name = ''.join(char for char in name if char.isalnum() or char.isspace() or char == ")" or char == "(").strip()
else:
name = ''.join(char for char in name if char.isalnum() or char.isspace() or char == "." or char == ")" or char == "(").strip()
name = ''.join(char for char in name if char.isalnum() or char.isspace() or char == "." or char == ")" or char == "(" or char == "-" or char == "_").strip()
name = re.sub(r'\s+', ' ', name)
# Remove or replace other SharePoint-invalid characters if necessary
@ -138,7 +141,7 @@ if not name_id or not files_id:
items = get_all_items(board_id, monday)
for i,item in enumerate(tqdm(items)):
if i>123:
if i>329:
item_name = item["name"]
item_name = sanitize_name(item_name, ignore_dot=True)
print(f"Item name is {item_name}")
@ -157,6 +160,4 @@ for i,item in enumerate(tqdm(items)):
if to_upload:
upload_to_sharepoint(to_upload, item_name)
# Liv green # Cocuun # Wates

View file

@ -1,7 +1,7 @@
from etl.utils.logger import Logger
import logging
import pymupdf
from etl.pdfReader.sitenotes import QuidosSiteNotesExtractor, CSR
from etl.pdfReader.sitenotes import QuidosSiteNotesExtractor, CSR, ConditionReport
from etl.pdfReader.reportType import ReportType
class pdfReaderToText():
@ -40,6 +40,8 @@ class pdfReaderToText():
self.type = ReportType.ENERGY_PERFORMANCE_REPORT
elif "Chartered Surveyor Report: Recommending Extraction of Defective Cavity Wall Insulation " in self.text_list:
self.type = ReportType.CHARTED_SURVEYOR_REPORT
elif "Osmosis ACD NEW PAS 2035 Condition Report".lower() in self.text_list[0].lower():
self.type = ReportType.OSMOSIS_CONDITION_PAS_2035_REPORT
else:
pass
return self.type
@ -51,4 +53,6 @@ class pdfReaderToText():
return QuidosSiteNotesExtractor(self.text_list)
elif self.type == ReportType.CHARTED_SURVEYOR_REPORT:
return CSR(self.text_list)
elif self.type == ReportType.OSMOSIS_CONDITION_PAS_2035_REPORT:
return ConditionReport(self.text_list)

View file

@ -6,4 +6,5 @@ class ReportType(Enum):
CHARTED_SURVEYOR_REPORT = "charted_surveyor_report"
ENERGY_PERFORMANCE_REPORT = "energy_performance_report"
U_VALUE_CALCULATOR_REPORT = "u_value_calculator_report"
OVERWRITING_U_VALUE_DECLARATION_FORM = "overwriting_u_value_declaration_form"
OVERWRITING_U_VALUE_DECLARATION_FORM = "overwriting_u_value_declaration_form"
OSMOSIS_CONDITION_PAS_2035_REPORT = "osmosis_condition_pas_2035_report"

View file

@ -8,7 +8,16 @@ from etl.transform.preSiteNoteTypes import (
WindTurbine, OtherDetails, Windows, Heating, HeatingSystemControls,
HeatingType, Insulation
)
from etl.transform.conditionReportTypes import (
ConditionReport, AssessorDetails, InspectionAndProject, TheProperty,
MainElevation, Elevation, ElevationInfo, PropertyAccess, ExternalElevation, ExternalElevationFront,
ExternalElevationGableOne, ExternalElevationGableTwo, ExternalElevationRear, ConservatoryOrOutbuilding,
AccessAndElevations, Hallway, RoomInfo, WindowsInfo, VentilationInfo, LivingRoom, DiningRoom, Kitchen, Rooms,
Utility, WC, Landing, Bedroom, Bathroom, LoftSpace, RoomInRoof, HeatingSystem, GeneralConditionHeatingSystem,
MainHeatingOne, MainHeatingTwo, SecondaryHeating, HeatingByRoom, Renewables, Occupant, EnergyUse, Heating, ShowerAndBath, FridgeAndFreezers, Cooker, TumbleDryer
)
from datetime import datetime
from pprint import pprint
class SiteNotesExtractor():
def __init__(self, data_list):
@ -18,6 +27,25 @@ class SiteNotesExtractor():
return [i for i, v in enumerate(lst) if v == value][x]
except IndexError:
return None # Return None if the value does not occur twice
def get_x_value(self, lst, value, x=1):
index = self.get_x_occurance(lst, value, x)
return lst[index+1]
def get_next_value(self, lst, value, avoid=[]):
get_value = lambda key: None if lst[lst.index(value) + 1] in avoid else lst[lst.index(key) + 1]
return get_value(value)
def get_next_value_greedy(self, lst, value, upto):
index = lst.index(value) + 1
str = ""
for i in range(upto):
str += lst[index+i]
str += " "
return str[:-1]
def two_columns_processor(self, data, sub_titles_to_gather, avoid, indexAdd = 1):
def get_value(key):
@ -58,10 +86,583 @@ class CSR(SiteNotesExtractor):
self.insulation_info = Insulation(
type=dict_.get('detailed_description_of_existing_cavity_wall_insulation_', "")
) if dict_ is not None else None
class ConditionReport(SiteNotesExtractor):
def __init__(self, data_list):
super().__init__(data_list)
self.type = ReportType.OSMOSIS_CONDITION_PAS_2035_REPORT
self.setup()
def setup(self):
assesor_details, inspection_and_project, the_property, main_elevation, elevations = self.get_section_1()
access_and_elevations = self.get_section_2()
rooms = self.get_section_3()
general_condition_of_heating_system, main_heating_one, main_heating_two, secondary_heating, heating_by_room, renewables = self.get_section_4()
occupants, energy_use, heating, shower_and_bath, appliances, fridge_and_freezers, cooker, tumble_dryer = self.get_section_5()
site_name, reference_code, address, postcode = self.get_section_0()
# TODO: Complete this function
self.make_condition_report_object(
assesor_details,
inspection_and_project,
the_property,
main_elevation,
elevations,
access_and_elevations,
rooms,
)
def get_section_0(self):
data = self.get_data_between("Project Site Name", "1. General Information")
site_name = self.get_next_value(data, "Project Site Name")
reference_code = self.get_next_value(data, "Property Reference Code")
address = self.get_next_value(data, "Property Address")
postcode = self.get_data_between("Postcode", "Main Image")[1:]
postcode = " ".join(postcode)
return site_name, reference_code, address, postcode
def get_section_1(self):
assessor_details = self.get_assessor_details()
inspection_and_project = self.get_inspection_and_project()
the_property = self.get_the_property()
main_elevation = self.get_main_elevation()
elevations = self.get_all_elevations()
return assessor_details, inspection_and_project, the_property, main_elevation, elevations
def get_assessor_details(self):
data = self.get_data_between("1.1 Assessor details","1.2 Inspection & Project")
assessor_name = self.get_next_value(data, "Assessor Name & ID")
elmhirst_id = self.get_next_value(data, "Please enter name & Elmhurst ID")
return AssessorDetails(assessor_name_and_id=assessor_name, elmhurst_id=elmhirst_id)
def get_inspection_and_project(self):
data = self.get_data_between("1.2 Inspection & Project", "1.3 The property")
date = self.get_next_value(data, "Inspection Date")
return InspectionAndProject(inspection_date=date)
def get_the_property(self):
data = self.get_data_between("1.3 The property", "Main elevation")
return TheProperty(
house_type=self.get_next_value(data, "House Type"),
on_which_floor_is_the_flat_located = self.get_next_value(data, "On which floor is the flat located?"),
is_there_a_corridor = True if self.get_next_value(data, "Is there a corridor?").lower() == "yes" else False,
is_it_heated = True if self.get_next_value(data, "Is it heated?").lower() == "yes" else False,
it_there_a_balcony = True if self.get_next_value(data, "Is there a balcony?").lower() == "yes" else False,
classification_type = self.get_next_value(data, "Classification Type"),
orientation_front_elevation = self.get_next_value(data, "Orientation (front elevation)"),
orientation_in_degrees_front_elevation = self.get_next_value(data, "Orientation in degrees (front elevation)"),
exposure_zone = self.get_next_value(data, "Exposure Zone"),
main_wall_construction = self.get_next_value(data, "Main Wall Construction")
)
def get_main_elevation(self):
data = self.get_data_between("Main elevation", "Elevation")
elevation = ElevationInfo(
elevation_type=self.get_next_value(data, "Elevation type"),
cavity_wall_depth=self.get_next_value(data, "Cavity wall depth (in mm)"),
is_insulation_present=True if self.get_next_value(data, "Is insulation present?").lower() == "yes" else False,
insulation_type= self.get_next_value(data, "Insulation type"),
)
return MainElevation(
elevation_info=elevation
)
def get_all_elevations(self):
elevations = []
i = 1
while(f"Elevation {i}" in self.raw_data):
i += 1
no_of_elevation = i-1
print(f"There are {no_of_elevation} elevation")
for i in range(no_of_elevation):
data = self.get_data_between(f"Elevation {i+1}", "2. Access & Elevations")
elevations.append(ElevationInfo(
elevation_type=self.get_next_value(data, "Elevation type"),
cavity_wall_depth=self.get_next_value(data, "Cavity wall depth (in mm)"),
is_insulation_present=True if self.get_next_value(data, "Is insulation present?").lower() == "yes" else False,
insulation_type= self.get_next_value(data, "Insulation type"),
))
return Elevation(
info=elevations,
protected_conservatory_or_aonb=self.get_next_value(data, "the Significance Survey below is required."),
material_type=self.get_next_value(data, "Material Type"),
are_there_any_visible_signs_of_existing_wall_insulation=self.get_next_value(data, "Are there any visible signs of existing wall insulation?"),
does_the_existing_ground_level_on_any_elevation_bridge_the_dpc=True if self.get_next_value(data, "DPC?").lower() == "yes" else False,
)
def get_section_2(self):
pa = self.get_property_access()
front,one,back, two = self.get_external_elevation()
co = self.get_conservatory_or_outbuilding()
return AccessAndElevations(
property_access=pa,
external_elevation_front=front,
external_elevation_back=back,
external_elevation_gable_one=one,
external_elevation_gable_two=two,
conservatory_or_out_building=co,
)
def get_property_access(self):
data = self.get_data_between("2.1 Property Access", "2.2 External Elevations")
property_access = PropertyAccess(
are_there_any_road_restriction_in_the_locality=True if self.get_next_value(data, "locality?").lower() == "yes" else False,
is_on_street_parking_available=True if self.get_next_value(data, "Is on-street parking available?").lower() == "yes" else False,
are_there_any_overhead_wires_or_cables=True if self.get_next_value(data, "Are there any overhead wires or cables?") == "yes" else False,
is_the_access_gated=True if self.get_next_value(data, "Is the access gated?").lower() == "yes" else False,
is_there_restricted_space_for_contractors_to_access_the_wall_area=True if self.get_next_value(data, "area?").lower() == "yes" else False,
is_there_restricted_space_for_contractors_to_access_the_roof_area=True if self.get_x_value(data, "area?", 1).lower() == "yes" else False,
is_there_more_than_1_point_5_meters_in_width_to_fence_or_neighbouring_boundary_along_the_full_gable_elevation=True if self.get_next_value(data, "boundary along the full gable elevation?").lower() == "yes" else False,
is_access_to_the_rear_provided_by_use_of_a_ginnel=True if self.get_next_value(data, "Is access to the rear provided by use of a ginnel?").lower() == "yes" else False,
is_access_to_the_rear_provided_by_use_of_a_secured_alleyway=True if self.get_next_value(data, "Is access to the rear provided by use of a secured alleyway?").lower() == "yes" else False,
)
return property_access
def get_external_elevation(self):
# Front
data = self.get_data_between("2.2.1. External Elevation - Front", '2.2.2. External Elevation - Gable 1')
ee = ExternalElevation(
structural_defects_of_elevation=self.get_next_value(data, "Structural defects of elevation"),
does_any_structural_defect_need_resolving_before_retrofit=True if self.get_next_value(data,"Does any structural defect need resolving before retrofit?").lower() == "yes" else False,
are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework=True if self.get_next_value(data, "rainwater goods or pipework?").lower() == "yes" else False,
are_there_any_visible_signs_of_movement=True if self.get_next_value(data, "Are there any visible signs of movement?").lower() == "yes" else False,
are_there_any_visible_signs_of_cracking_to_the_existing_external_finish=True if self.get_next_value(data, "external finish?").lower() == "yes" else False,
)
front = ExternalElevationFront(
external_elevation=ee
)
# Gable 1
data = self.get_data_between("2.2.2. External Elevation - Gable 1", "2.2.3. External Elevation - Rear")
state = True if self.get_next_value(data, "cracking)").lower() == "yes" else False
if state:
gable_one = ExternalElevationGableOne(
do_all_answers_for_the_front_elevation_apply_to_this_wall=state
)
else:
gable_one_elevation = ExternalElevation(
structural_defects_of_elevation=self.get_next_value(data, "Structural defects of elevation"),
does_any_structural_defect_need_resolving_before_retrofit=True if self.get_next_value(data,"Does any structural defect need resolving before retrofit?").lower() == "yes" else False,
are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework=True if self.get_next_value(data, "rainwater goods or pipework?").lower() == "yes" else False,
are_there_any_visible_signs_of_movement=True if self.get_next_value(data, "Are there any visible signs of movement?").lower() == "yes" else False,
are_there_any_visible_signs_of_cracking_to_the_existing_external_finish=True if self.get_next_value(data, "external finish?").lower() == "yes" else False,
)
gable_one = ExternalElevationGableOne(
do_all_answers_for_the_front_elevation_apply_to_this_wall=state,
external_elevation=gable_one_elevation
)
# Rear
data = self.get_data_between("2.2.3. External Elevation - Rear", "2.2.4. External Elevation - Gable 2")
state = True if self.get_next_value(data, "cracking)").lower() == "yes" else False
if state:
rear = ExternalElevationRear(do_all_answers_for_the_front_elevation_apply_to_this_wall=state)
else:
elevation = ExternalElevation(
structural_defects_of_elevation=self.get_next_value(data, "Structural defects of elevation"),
does_any_structural_defect_need_resolving_before_retrofit=True if self.get_next_value(data,"Does any structural defect need resolving before retrofit?").lower() == "yes" else False,
are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework=True if self.get_next_value(data, "rainwater goods or pipework?").lower() == "yes" else False,
are_there_any_visible_signs_of_movement=True if self.get_next_value(data, "Are there any visible signs of movement?").lower() == "yes" else False,
are_there_any_visible_signs_of_cracking_to_the_existing_external_finish=True if self.get_next_value(data, "external finish?").lower() == "yes" else False,
)
rear = ExternalElevationRear(
do_all_answers_for_the_front_elevation_apply_to_this_wall=state,
external_elevation=elevation
)
# Gable 2
data = self.get_data_between("2.2.4. External Elevation - Gable 2", "2.3. Conservatory or Outbuilding")
pprint(self.raw_data)
state = True if self.get_next_value(data, "Is there a 4th external elevation?").lower() == "yes" else False
if state is False:
gable_two = ExternalElevationGableTwo(is_there_a_fourth_external_elevation=state)
else:
gable_two_elevation = ExternalElevation(
structural_defects_of_elevation=self.get_next_value(data, "Structural defects of elevation"),
does_any_structural_defect_need_resolving_before_retrofit=True if self.get_next_value(data,"Does any structural defect need resolving before retrofit?").lower() == "yes" else False,
are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework=True if self.get_next_value(data, "rainwater goods or pipework?").lower() == "yes" else False,
are_there_any_visible_signs_of_movement=True if self.get_next_value(data, "Are there any visible signs of movement?").lower() == "yes" else False,
are_there_any_visible_signs_of_cracking_to_the_existing_external_finish=True if self.get_next_value(data, "external finish?").lower() == "yes" else False,
)
gable_two = ExternalElevationGableTwo(
is_there_a_fourth_external_elevation=state,
external_elevation=gable_two_elevation
)
return front, gable_one, rear, gable_two
def get_conservatory_or_outbuilding(self):
data = self.get_data_between("2.3. Conservatory or Outbuilding", "3. Rooms")
return ConservatoryOrOutbuilding(
is_there_a_conservatory=True if self.get_next_value(data, "Is there a Conservatory?").lower() == "yes" else False,
is_there_a_cellar_present=True if self.get_next_value(data, "Is there a cellar present?").lower() == "yes" else False,
is_there_an_outbuilding=True if self.get_next_value(data, "Is there an Outbuilding?").lower() == "yes" else False,
)
def get_section_3(self):
hallway = self.get_hallway()
living_room = self.get_living_room()
dining_room = self.get_dining_room()
kitchen = self.get_kitchen()
utility = self.get_utility()
wc = self.get_wc()
landing = self.get_landing()
bedrooms = self.get_bedrooms()
bathrooms = self.get_bathroom()
loft_space = self.get_loft_space()
room_in_roof = self.get_room_in_roof()
return Rooms(
hallway=hallway,
living_room=living_room,
dining_room=dining_room,
kitchen=kitchen,
utility=utility,
wash_chamber=wc,
landing=landing,
bedrooms=bedrooms,
bathrooms=bathrooms,
loft_space=loft_space,
room_in_roof=room_in_roof,
)
def get_room_info(self, data):
ventilation = VentilationInfo(
is_there_a_ventilation_system_present_in_the_room=True if self.get_next_value(data, "Is there a ventilation system present in the room?").lower() == "yes" else False,
are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room=True if self.get_next_value(data, "excessive condensation within the room?").lower() == "yes" else False,
are_there_sufficient_undercuts_on_the_closed_door=self.get_next_value(data, "- min 7.6mm per 1000mm width door)?"),
is_there_any_open_flue_heating_appliances_within_the_room=True if self.get_next_value(data, "Is there any open flue heating appliances within the room?").lower() == "yes" else False,
)
windows_state = True if self.get_next_value(data, "Does the room have any windows?").lower() == "yes" else False
if windows_state:
windows = WindowsInfo(
does_the_room_have_any_windows=windows_state,
condition_of_the_windows=self.get_next_value(data, "Condition of the windows"),
do_the_windows_have_trickle_vents=True if self.get_next_value(data, "Do the windows have trickle vents?").lower() == "yes" else False,
input_trickle_vent_product_code_or_measurement=self.get_next_value(data, "in both cases."),
are_the_windows_openable=True if self.get_next_value(data, "Are the windows openable?").lower() == "yes" else False,
)
else:
windows = WindowsInfo(
does_the_room_have_any_windows=windows_state
)
return RoomInfo(
overall_condition_of_the_room=self.get_next_value(data, "Overall condition of the room"),
does_the_room_have_any_defects=self.get_next_value(data, "cracking in walls, etc.)"),
windows_info=windows,
ventilation_info=ventilation,
)
def get_hallway(self):
data = self.get_data_between("Hallway", "Living Room")
room = self.get_room_info(data)
return Hallway(
is_there_a_hallway=True if self.get_next_value(data, "Is there a hallway?").lower() == "yes" else False,
room_info=room,
)
def get_living_room(self):
data = self.get_data_between("Living Room", "Dining Room")
room = self.get_room_info(data)
return LivingRoom(
room_info=room,
)
def get_dining_room(self):
data = self.get_data_between("Dining Room", "Kitchen")
dining_room_state = True if self.get_next_value(data, "Is there a dining room?").lower() == "yes" else False
if dining_room_state:
room = self.get_room_info(data)
else:
room = None
return DiningRoom(
is_there_a_dining_room=dining_room_state,
room_info=room,
)
def get_kitchen(self):
data = self.get_data_between("Kitchen", "Utility")
room = self.get_room_info(data)
return Kitchen(
room_info=room,
is_there_a_cooker_hood_present_in_the_room=True if self.get_next_value(data, "Is there a cooker hood present in the room?").lower() == "yes" else False,
)
def get_utility(self):
data = self.get_data_between("Utility", "WC")
utility_state = True if self.get_next_value(data, "Is there a utility room?").lower() == "yes" else False
if utility_state:
room = self.get_room_info(data)
else:
room = None
return Utility(
is_there_a_utility_room=utility_state,
room_info=room
)
def get_wc(self):
data = self.get_data_between("WC", "Landing")
wc_state = True if self.get_next_value(data, "Is there a separated WC?").lower() == "yes" else False
if wc_state:
room = self.get_room_info(data)
else:
room = None
return WC(
is_there_a_seperated_wc=wc_state,
room_info=room
)
def get_landing(self):
data = self.get_data_between("Landing", "Bedroom")
landing_state = True if self.get_next_value(data, "Is there a landing?").lower() == "yes" else False
if landing_state:
room_info = self.get_room_info(data)
else:
room_info = None
return Landing(
is_there_a_landing=landing_state,
room_info=room_info
)
def get_bedrooms(self):
bedrooms = []
i = 1
while(f"Bedroom {i}" in self.raw_data):
i += 1
no_of_bedrooms = i-1
print(f"There are {no_of_bedrooms} bedrooms")
for i in range(no_of_bedrooms):
data = self.get_data_between(f"Bedroom {i+1}", "Bathroom")
roominfo = self.get_room_info(data)
bedrooms.append(Bedroom(
double_or_single_bedroom=self.get_next_value(data, "Double or Single Bedroom?"),
room_info=roominfo
))
return bedrooms
def get_bathroom(self):
bathrooms = []
i = 1
while(f"Bathroom {i}" in self.raw_data):
i += 1
no_of_bathrooms = i-1
print(f"There are {no_of_bathrooms} bathrooms")
for i in range(no_of_bathrooms):
data = self.get_data_between(f"Bathroom {i+1}", "Loft Space")
roominfo = self.get_room_info(data)
bathrooms.append(Bathroom(
is_this_an_ensuite_bathroom=self.get_next_value(data, "Is this an ensuite bathroom?"),
room_info=roominfo
))
return bathrooms
def get_loft_space(self):
data = self.get_data_between("Loft Space", "Room in Roof")
return LoftSpace(
is_the_main_loft_space_accessible=self.get_next_value(data, "Is the main loft space accessible?"),
is_there_more_than_one_loft_space=True if self.get_next_value(data, "extension)?").lower() == "yes" else False,
)
def get_room_in_roof(self):
data = self.get_data_between("Room in Roof", "4. Heating System")
room_in_roof_state = True if self.get_next_value(data, "Is there a room in roof?").lower() == "yes" else False
room_info = None
if room_in_roof_state:
room_info = self.get_room_info(data)
return RoomInRoof(
is_there_a_room_in_roof=room_in_roof_state,
room_info=room_info
)
def get_section_4(self):
general_condition_of_heating_system = self.get_general_condition_of_heating_system()
main_heating_one = self.get_main_heating_one()
main_heating_two = self.get_main_heating_two()
secondary_heating = self.get_secondary_heating()
heating_by_room = self.get_heating_by_room()
renewables = self.get_renewables()
return general_condition_of_heating_system, main_heating_one, main_heating_two, secondary_heating, heating_by_room, renewables
def get_main_heating_one(self):
data = self.get_data_between("Main Heating 1", "Main Heating 2")
return MainHeatingOne(
as_defined_by=self.get_next_value(data, "As defined by"),
fuel=self.get_next_value(data, "Fuel:"),
type=self.get_next_value_greedy(data, "Type", 2)
)
def get_main_heating_two(self):
data = self.get_data_between("Main Heating 2", "Secondary Heating")
return MainHeatingTwo(
is_there_a_main_heating_two=True if self.get_next_value(data, "Is there a Main Heating 2?").lower() == "yes" else False,
)
def get_secondary_heating(self):
data = self.get_data_between("Secondary Heating", "Heating by room")
return SecondaryHeating(
is_there_a_secondary_heating=True if self.get_next_value(data, "Is there a Secondary Heating?").lower() == "yes" else False,
fuel=self.get_next_value(data, "Fuel"),
electric_heating_type=self.get_next_value_greedy(data, "Type", 2),
gas_heating_type=self.get_x_value(data, "Type", 1),
)
def get_heating_by_room(self):
data = self.get_data_between("Heating by room", "Renewables")
list_of_main_heating_system_by_one = self.get_data_between("Rooms heated by Main System 1:", "Rooms Heated by Main System 2")[1:]
list_of_main_heating_system_by_two = self.get_data_between("Rooms Heated by Main System 2", "Rooms heated by Secondary Heating:")[1:]
list_of_rooms_heated_by_secondary_heating = self.get_data_between("Rooms heated by Secondary Heating:", "Are there any partially heated rooms?")[1:]
are_there_any_partially_heated_rooms = True if self.get_next_value(data, "Are there any partially heated rooms?").lower() == "yes" else False
are_there_any_unheated_rooms = True if self.get_next_value(data, "Are there any unheated rooms?").lower() == "yes" else False
list_of_partially_heated_rooms = self.get_data_between("Are there any partially heated rooms?", "Are there any unheated rooms?")[2:]
list_of_unheated_rooms = self.get_data_between("Are there any unheated rooms?", "Renewables")[2:]
def remove_special_character(lst):
return_lst = []
for i, item in enumerate(lst):
if '\xa0' in item or 'Rooms' in item or len(item) == 1 or item.lower() == "None".lower():
pass
else:
return_lst.append(item)
return return_lst
return HeatingByRoom(
rooms_heated_by_main_system_one=remove_special_character(list_of_main_heating_system_by_one),
rooms_heated_by_main_system_two=remove_special_character(list_of_main_heating_system_by_two),
rooms_heated_by_secondary_heating=remove_special_character(list_of_rooms_heated_by_secondary_heating),
are_there_any_partially_heated_rooms=are_there_any_partially_heated_rooms,
are_there_any_unheated_rooms=are_there_any_unheated_rooms,
list_of_partially_heated_rooms=remove_special_character(list_of_partially_heated_rooms),
unheated_rooms=remove_special_character(list_of_unheated_rooms),
)
def get_renewables(self):
data = self.get_data_between("Renewables", "5. Occupancy Assessment")
return Renewables(
is_there_any_renewable_energy_system_in_place=True if self.get_next_value(data, "Is there any renewable energy system in place?").lower() == "yes" else False,
suitable_roof_orientation_for_solar_pv_water=self.get_next_value(data, "Suitable roof orientation for Solar PV/water:"),
is_there_a_water_tank=True if self.get_next_value(data, "Is there a Water Tank?").lower() == "yes" else False,
type=self.get_next_value(data, "Type"),
size=self.get_next_value(data, "Size"),
tank_location=self.get_next_value(data, "Tank Location"),
is_the_tank_insulated=True if self.get_next_value(data, "Is the tank Insulated") else False,
type_of_insulation=self.get_next_value(data, "Type of Insulation"),
thickness_of_insulation_in_mm=int(self.get_next_value(data, "Thickness of Insulation (mm)")),
)
def get_general_condition_of_heating_system(self):
data = self.get_data_between("4. Heating System", "Main Heating 1")
return GeneralConditionHeatingSystem(
is_the_heating_system_in_working_order=True if self.get_next_value(data, "Is the Heating System in working order?") else False,
does_the_occupant_have_a_smart_meter=True if self.get_next_value(data, "Does the occupant have a Smart Meter?") else False,
are_there_any_smart_monitoring_devices=True if self.get_next_value(data, "Are there any smart monitoring devices (Switchee etc)?") else False,
are_the_gas_and_electricity_meters_accessible=True if self.get_next_value(data, "Are the Gas and Electricity Meters accessible?") else False,
dual_or_single_electric_meter=self.get_next_value(data, "Dual or single electric meter?"),
)
def get_section_5(self):
occupants = self.get_occupants()
energy_use = self.get_energy_use()
heating = self.get_heating()
shower_and_bath = self.get_shower_and_bath()
appliances = self.get_appliances()
fridge_and_freezers = self.get_fridge_and_freezers()
cooker = self.get_cooker()
tumble_dryer = self.get_tumble_dryer()
return occupants, energy_use, heating, shower_and_bath, appliances, fridge_and_freezers, cooker, tumble_dryer
def get_occupants(self):
data = self.get_data_between("Occupants", "Energy use")
second_data = self.get_data_between("Tumble dryer", "Media summary")
return Occupant(
name=self.get_next_value(second_data, "Name of the occupant:"),
have_evidence_of_12_months_of_fuel_bill_data= True if self.get_next_value(second_data, "Have you evidenced 12 months of fuel bill data?").lower() == "yes" else False,
total_number_of_occupants=int(self.get_next_value(data, "Total number of occupants:")),
no_of_adult_occupants=int(self.get_next_value(data, "No. of Adult Occupants (18+)")),
no_of_child_occupants=int(self.get_next_value(data, "No. of Child Occupants (Under 18)")),
no_of_occupant_of_a_pensionable_age=int(self.get_next_value(data, "No. of occupant of a pensionable age")),
are_there_any_vulnerable_people=True if self.get_next_value(data, "Are there any vulnerable people?").lower() == "yes" else False,
is_there_anyone_with_a_disability=True if self.get_next_value(data, "Is there anyone with a disability?").lower() == "yes" else False,
status_of_occupant=self.get_next_value(data, "Status of the occupant:"),
landlord_has_written_confirmation_that_the_tenent_agrees_to_the_assessment_been_supplied=True if self.get_next_value(data, "the assessment been supplied").lower() == "yes" else False,
)
def get_energy_use(self):
data = self.get_data_between("Energy use", "Heating")
return EnergyUse(
property_tenure=self.get_next_value(data, "Property tenure:"),
who_is_the_electricity_payer=self.get_next_value(data, "Who is the electricity bill payer?")
)
def get_heating(self):
data = self.get_data_between("Heating", "Shower & bath")
return Heating(
room_stat_in_temperature_in_celsius=self.get_next_value(data, "Room Stat Temperature (in °C)", avoid=["Room Stat Location", '\xa0']),
room_stat_location=self.get_next_value(data, "Room Stat Location", avoid = ["Is the heating pattern known?", '\xa0']),
is_the_heating_pattern_known=self.get_next_value(data, "Is the heating pattern known?", avoid=["Shower & bath", '\xa0']),
)
def get_shower_and_bath(self):
data = self.get_data_between("Shower & bath", "Appliances")
return ShowerAndBath(
shower_type=self.get_next_value(data, "Shower Type:"),
do_you_know_the_no_of_showers_per_day_per_week=True if self.get_next_value(data, "Do you know the number of showers per day or per week?").lower() == "yes" else False,
please_input_no_of_showers_and_specify_a_day_or_a_week=self.get_next_value(data, '"per week"'),
do_you_know_the_number_of_baths_per_day_or_per_week=self.get_next_value(data, "Do you know the number of baths per day or per week?"),
)
def get_appliances(self):
print("Skipped appliances due to not having this example yet")
def get_fridge_and_freezers(self):
data = self.get_data_between("Fridge & freezers", "Cooker")
return FridgeAndFreezers(
no_of_stand_alone_seperate_fridges=int(self.get_next_value(data,"No. of Standalone (separate) Fridges:")),
no_of_stand_alone_seperate_freezers=int(self.get_next_value(data, "No. of Standalone (separate) Freezers:")),
no_of_stand_alone_or_integrated_fridge_freezers=int(self.get_next_value(data, "No. of Standalone or Integrated Fridge Freezers:"))
)
def get_cooker(self):
data = self.get_data_between("Cooker", "Tumble dryer")
return Cooker(
cooker_type=self.get_next_value(data, "Cooker Type:"),
normal_large_range=self.get_next_value(data, "Normal - Large - Range"),
range_fuel=self.get_next_value(data, "Range Fuel:")
)
def get_tumble_dryer(self):
data = self.get_data_between("Tumble dryer", "Have you evidenced 12 months of fuel bill data?")
return TumbleDryer(
percentage_of_annual_use=int(self.get_next_value(data, "Percentage of annual use:")),
space_for_outdoor_drying=True if self.get_next_value(data,"Space for outdoor drying?").lower() == "yes" else False,
)
class QuidosSiteNotesExtractor(SiteNotesExtractor):
def __init__(self, data_list):
super().__init__(data_list)
@ -125,7 +726,7 @@ class QuidosSiteNotesExtractor(SiteNotesExtractor):
self.survey_information = PreSiteNotesSummaryInfo(
reference_number = get_value('Reference Number'),
epc_language = get_value('EPC Language'),
uprn = get_value('UPRN'),
uprn = get_value('UPRN').lstrip('0'),
postcode = get_value('Postcode'),
region = get_value('Region'),
address = ','.join(get_value('Address').split(',')[:-1]).strip(),
@ -294,7 +895,6 @@ class QuidosSiteNotesExtractor(SiteNotesExtractor):
no_of_heated_rooms = int(get_value('Number of Heated Habitable Rooms')),
heated_basement = False if get_value('Heated Basement') == "NO" else True,
conservatory_type = get_value('Conservatory Type'),
terrain_type = get_value('Terrain Type'),
percentage_of_draught_proofed= int(get_value('Percentage of Draught Proofed(%)')),
main_property=PropertyDetail(
age_band=age_bands[0],

View file

@ -13,9 +13,8 @@ from datetime import datetime, timedelta
def previous_monday():
today = datetime.today()
last_monday = today - timedelta(days=today.weekday() + 7) # Go back to last week's Monday
# return f"W.C. 31.09.2000"
return f"W.C. {last_monday.strftime('%d.%m.%Y')}"
monday = today - timedelta(days=today.weekday()) # weekday() = 0 for Monday
return f"W.C. {monday.strftime('%d.%m.%Y')}"
WEEK_COMMENCING = os.getenv("WEEK_COMMENCING", previous_monday())

View file

@ -21,6 +21,7 @@ class surveyedDataProcessor():
self.files = files
self.pre_site_note = None
self.csr = None
self.condition_report = None
self.identify_files()
self.hubspot_deal_id = None
@ -34,6 +35,8 @@ class surveyedDataProcessor():
self.address = self.pre_site_note.survey_information.address
elif pdf.type == ReportType.CHARTED_SURVEYOR_REPORT:
self.csr = pdf.get_reader()
elif pdf.type == ReportType.OSMOSIS_CONDITION_PAS_2035_REPORT:
self.condition_report = pdf.get_reader()
def load_pre_site_notes_summary_table(self, db_session):
summary_data = self.pre_site_note.survey_information.model_dump()

View file

@ -0,0 +1,284 @@
from etl.transform.preSiteNoteTypes import BaseModel, Optional, List
class AssessorDetails(BaseModel):
assessor_name_and_id: str
elmhurst_id: str
class InspectionAndProject(BaseModel):
inspection_date: str
class TheProperty(BaseModel):
house_type: str
on_which_floor_is_the_flat_located: str
is_there_a_corridor: bool
is_it_heated: bool
it_there_a_balcony: bool
classification_type: str
orientation_front_elevation: str
orientation_in_degrees_front_elevation: str
exposure_zone: str
main_wall_construction: str
class ElevationInfo(BaseModel):
elevation_type: str
cavity_wall_depth: str
is_insulation_present: bool
insulation_type: str
class MainElevation(BaseModel):
elevation_info: ElevationInfo
class Elevation(BaseModel):
info: List[ElevationInfo]
protected_conservatory_or_aonb: bool
material_type: str
are_there_any_visible_signs_of_existing_wall_insulation: str
does_the_existing_ground_level_on_any_elevation_bridge_the_dpc: bool
class GeneralInformation(BaseModel):
assessor_details: AssessorDetails
inspection_and_project: InspectionAndProject
the_property: TheProperty
main_elevation: MainElevation
elevations: Elevation
class PropertyAccess(BaseModel):
are_there_any_road_restriction_in_the_locality: bool
is_on_street_parking_available: bool
are_there_any_overhead_wires_or_cables: bool
is_the_access_gated: bool
is_there_restricted_space_for_contractors_to_access_the_wall_area: bool
is_there_restricted_space_for_contractors_to_access_the_roof_area: bool
is_there_more_than_1_point_5_meters_in_width_to_fence_or_neighbouring_boundary_along_the_full_gable_elevation: bool
is_access_to_the_rear_provided_by_use_of_a_ginnel: bool
is_access_to_the_rear_provided_by_use_of_a_ginnel: bool
is_access_to_the_rear_provided_by_use_of_a_secured_alleyway: bool
class ExternalElevation(BaseModel):
structural_defects_of_elevation: str
does_any_structural_defect_need_resolving_before_retrofit: bool
are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework: bool
are_there_any_visible_signs_of_movement: bool
are_there_any_visible_signs_of_cracking_to_the_existing_external_finish: bool
class ExternalElevationFront(BaseModel):
external_elevation: ExternalElevation
class ExternalElevationGableOne(BaseModel):
do_all_answers_for_the_front_elevation_apply_to_this_wall: bool
external_elevation: Optional[ExternalElevation] = None
class ExternalElevationRear(BaseModel):
do_all_answers_for_the_front_elevation_apply_to_this_wall: bool
external_elevation: Optional[ExternalElevation] = None
class ExternalElevationGableTwo(BaseModel):
is_there_a_fourth_external_elevation: bool
external_elevation: Optional[ExternalElevation] = None
class ConservatoryOrOutbuilding(BaseModel):
is_there_a_conservatory: bool
is_there_a_cellar_present: bool
is_there_an_outbuilding: bool
class AccessAndElevations(BaseModel):
property_access: PropertyAccess
external_elevation_front: ExternalElevationFront
external_elevation_back: ExternalElevationRear
external_elevation_gable_one: ExternalElevationGableOne
external_elevation_gable_two: ExternalElevationGableTwo
conservatory_or_out_building: ConservatoryOrOutbuilding
class VentilationInfo(BaseModel):
is_there_a_ventilation_system_present_in_the_room: bool
are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room: bool
are_there_sufficient_undercuts_on_the_closed_door: str
is_there_any_open_flue_heating_appliances_within_the_room: bool
class WindowsInfo(BaseModel):
does_the_room_have_any_windows: bool
condition_of_the_windows: Optional[str] = None
do_the_windows_have_trickle_vents: Optional[bool] = None
are_the_windows_openable: Optional[bool] = None
input_trickle_vent_product_code_or_measurement: Optional[str] = None
class RoomInfo(BaseModel):
overall_condition_of_the_room: str
does_the_room_have_any_defects: str
are_there_any_sloped_ceiling_areas: Optional[bool] = None
windows_info: Optional[WindowsInfo] = None
ventilation_info: Optional[VentilationInfo] = None
class Hallway(BaseModel):
is_there_a_hallway: bool
room_info: Optional[RoomInfo]
class LivingRoom(BaseModel):
room_info: Optional[RoomInfo]
class DiningRoom(BaseModel):
is_there_a_dining_room: bool
room_info: Optional[RoomInfo]
class Kitchen(BaseModel):
room_info: Optional[RoomInfo]
is_there_a_cooker_hood_present_in_the_room: bool
class Utility(BaseModel):
is_there_a_utility_room: bool
room_info: Optional[RoomInfo]
class WC(BaseModel):
is_there_a_seperated_wc: bool
room_info: Optional[RoomInfo]
class Landing(BaseModel):
is_there_a_landing: bool
room_info: Optional[RoomInfo]
class Bedroom(BaseModel):
double_or_single_bedroom: str
room_info: Optional[RoomInfo]
class Bathroom(BaseModel):
is_this_an_ensuite_bathroom: bool
room_info: Optional[RoomInfo]
class LoftSpace(BaseModel):
is_the_main_loft_space_accessible: str
is_there_more_than_one_loft_space: bool
class RoomInRoof(BaseModel):
is_there_a_room_in_roof: bool
room_info: Optional[RoomInfo]
class Rooms(BaseModel):
hallway: Hallway
living_room: LivingRoom
dining_room: DiningRoom
kitchen: Kitchen
utility: Utility
wash_chamber: WC
landing: Landing
bedrooms: List[Bedroom]
bathrooms: List[Bathroom]
loft_space: LoftSpace
room_in_roof: RoomInRoof
class GeneralConditionHeatingSystem(BaseModel):
is_the_heating_system_in_working_order: bool
does_the_occupant_have_a_smart_meter: bool
are_there_any_smart_monitoring_devices: bool
are_the_gas_and_electricity_meters_accessible: bool
dual_or_single_electric_meter: str
class SecondaryHeating(BaseModel):
is_there_a_secondary_heating: bool
fuel: str
electric_heating_type: str
gas_heating_type: str
class MainHeatingOne(BaseModel):
as_defined_by: str
fuel: str
type: str
class MainHeatingTwo(BaseModel):
is_there_a_main_heating_two: bool
class HeatingByRoom(BaseModel):
rooms_heated_by_main_system_one: List[str]
rooms_heated_by_main_system_two: List[str]
rooms_heated_by_secondary_heating: List[str]
are_there_any_partially_heated_rooms: bool
partially_heated_rooms: Optional[List[str]] = []
are_there_any_unheated_rooms: bool
unheated_rooms: List[str]
class Renewables(BaseModel):
is_there_any_renewable_energy_system_in_place: bool
suitable_roof_orientation_for_solar_pv_water: str
is_there_a_water_tank: bool
type: str
size: str
tank_location: str
is_the_tank_insulated: bool
type_of_insulation: str
thickness_of_insulation_in_mm: int
class HeatingSystem(BaseModel):
general_condition: GeneralConditionHeatingSystem
main_heating_one: MainHeatingOne
main_heating_two: MainHeatingTwo
secondary_heating: SecondaryHeating
heating_by_room: HeatingByRoom
renewables: Renewables
class Occupant(BaseModel):
name: str
have_evidence_of_12_months_of_fuel_bill_data: bool
total_number_of_occupants: int
no_of_adult_occupants: int
no_of_child_occupants: int
no_of_occupant_of_a_pensionable_age: int
are_there_any_vulnerable_people: bool
is_there_anyone_with_a_disability: bool
status_of_occupant: str
landlord_has_written_confirmation_that_the_tenent_agrees_to_the_assessment_been_supplied: bool
class TumbleDryer(BaseModel):
percentage_of_annual_use: int
space_for_outdoor_drying: bool
class Cooker(BaseModel):
range_fuel: str
normal_large_range: str
cooker_type: str
class FridgeAndFreezers(BaseModel):
no_of_stand_alone_seperate_fridges: int
no_of_stand_alone_seperate_freezers: int
no_of_stand_alone_or_integrated_fridge_freezers: int
class Appliances(BaseModel):
find_out_with_one_in: str
class ShowerAndBath(BaseModel):
shower_type: str
do_you_know_the_no_of_showers_per_day_per_week: bool
please_input_no_of_showers_and_specify_a_day_or_a_week: str
do_you_know_the_number_of_baths_per_day_or_per_week: str
class Heating(BaseModel):
# TODO find one with an example of this one
room_stat_in_temperature_in_celsius: Optional[str] = None
room_stat_location: Optional[str] = None
is_the_heating_pattern_known: Optional[str] = None
class EnergyUse(BaseModel):
property_tenure: str
who_is_the_electricity_payer: str
class OccupantAssessment(BaseModel):
occupant: Occupant
energy_use: EnergyUse
heating: Heating
shower_and_bath: ShowerAndBath
appliances: Optional[Appliances]
fridge_and_freezers: FridgeAndFreezers
cooker: Cooker
tumble_dryer: TumbleDryer
class ConditionReport(BaseModel):
project_site_name: str
property_reference_code: str
property_address: str
postcode: str
general_information: GeneralInformation
access_and_elevations: AccessAndElevations
rooms: Rooms
heating_system: HeatingSystem
occupancy_assessment: OccupantAssessment

0
frontend/.env Normal file
View file

View file

@ -0,0 +1,2 @@
# DATABASE_URL=postgresql://postgres:makingwarmhomes@db:5432/postgres
DATABASE_URL=postgresql://postgres:makingwarmhomes@terraform-20250331175522503500000002.cdgzupxvdyp0.eu-west-2.rds.amazonaws.com:5432/surveyDB

1
frontend/.env.production Normal file
View file

@ -0,0 +1 @@
DATABASE_URL=postgresql://postgres:makingwarmhomes@terraform-20250331175522503500000002.cdgzupxvdyp0.eu-west-2.rds.amazonaws.com:5432/surveyDB

2
frontend/.gitignore vendored
View file

@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# .env*
# vercel
.vercel

File diff suppressed because it is too large Load diff

View file

@ -9,19 +9,25 @@
"lint": "next lint"
},
"dependencies": {
"dotenv": "^16.5.0",
"drizzle-orm": "^0.44.0",
"next": "15.3.2",
"pg": "^8.16.0",
"postgres": "^3.4.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.2"
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pg": "^8.15.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"drizzle-kit": "^0.31.1",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4",
"typescript": "^5"
}
}

View file

@ -0,0 +1,9 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not defined in environment variables");
}
const sqlClient = postgres(process.env.DATABASE_URL);
export const db = drizzle(sqlClient);

View file

@ -0,0 +1,10 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
export const buildings = pgTable('buildings', {
id: uuid('id').defaultRandom().primaryKey(),
address: text('address').notNull(),
postcode: text('postcode').notNull(),
UPRN: text('UPRN').notNull(),
landlordId: text('landlord_id').notNull(),
domnaId: text('domna_id').notNull(),
});

View file

@ -1,6 +1,55 @@
// app/page.tsx
import { db } from './db/db'; // adjust path if needed
import { buildings } from './db/schema/buildings';
type Building = {
id: string;
address: string;
postcode: string;
uprn: string;
landlordId: string;
domnaId: string;
};
export default async function Home() {
const buildingRows = await db.select().from(buildings);
const buildingsData: Building[] = buildingRows.map((b) => ({
id: b.id.toString(),
address: b.address,
postcode: b.postcode,
uprn: b.UPRN,
landlordId: b.landlordId,
domnaId: b.domnaId,
}));
export default function Home() {
return (
<h1>Hello Next.js!</h1>
<>
<h1>Buildings List</h1>
<table border={1} cellPadding={8} cellSpacing={0}>
<thead>
<tr>
<th>ID</th>
<th>Address</th>
<th>Postcode</th>
<th>UPRN</th>
<th>Landlord ID</th>
<th>Domna ID</th>
</tr>
</thead>
<tbody>
{buildingsData.map((b) => (
<tr key={b.id}>
<td>{b.id}</td>
<td>{b.address}</td>
<td>{b.postcode}</td>
<td>{b.uprn}</td>
<td>{b.landlordId}</td>
<td>{b.domnaId}</td>
</tr>
))}
</tbody>
</table>
</>
);
}